BerriAI/litellm · error · ValueError

Category file path '{path}' is outside the allowed categorie

Error message

Category file path '{path}' is outside the allowed categories directory '{categories_dir}'

What it means

ValueError from _assert_within_categories_dir raised when the realpath-resolved category file path does not share the categories directory prefix — the file genuinely escapes the litellm_content_filter module directory after symlink resolution or traversal. This is a deliberate security jail: category_file values are untrusted config inputs and must not read arbitrary files (e.g. ../../../../etc/passwd).

Source

Thrown at litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py:355

        return {
            "category": cat_config.get("category"),
            "enabled": cat_config.get("enabled", True),
            "action": cat_config.get("action"),
            "category_file": cat_config.get("category_file"),
        }

    @staticmethod
    def _assert_within_categories_dir(path: str, categories_dir: str) -> None:
        """Raise ValueError if path escapes the categories directory."""
        resolved: Final = os.path.realpath(path)
        allowed: Final = os.path.realpath(categories_dir)
        try:
            common: Final = os.path.commonpath([resolved, allowed])
        except ValueError:
            # commonpath() raises ValueError on Windows when paths span different drives
            raise ValueError(f"Category file path '{path}' is outside the allowed categories directory")
        if common != allowed:
            raise ValueError(
                f"Category file path '{path}' is outside the allowed categories directory '{categories_dir}'"
            )

    def _resolve_category_file_path(self, file_path: str) -> str:
        """
        Resolve a category file path that may be relative.

        Paths in policy templates (e.g. category_file) are often stored as
        relative paths like "litellm/proxy/.../policy_templates/file.yaml".
        These only work when the CWD is the project root. In production
        (Docker, installed packages, etc.) the CWD is different, so the
        file isn't found.

        Resolution order:
        1. Return as-is if absolute or already exists (jailed to module dir).
        2. Try joining the full path relative to this module's directory (jailed).
        3. Progressively strip leading path components and try each suffix
           relative to this module's directory (jailed).

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Place custom category files inside the litellm_content_filter module directory (a subdirectory of it is fine) and reference them with a relative path.
  2. If files legitimately live on a mounted volume outside the package (trusted environments only), set LITELLM_CONTENT_FILTER_ALLOW_EXTERNAL_PATHS=true — understand this disables the traversal jail.
  3. Fix the category_file value: remove ../ segments and absolute paths pointing outside the categories dir.
  4. Replace symlinks that point outside the module dir with real files inside it.

Example fix

# before
category_file: '../../../../etc/custom_policies/secret_category.yaml'

# after — file shipped inside the module dir
category_file: 'categories/secret_category.yaml'
Defensive patterns

Strategy: validation

Validate before calling

# Pre-flight the same jail the guardrail applies: realpath containment
import os

def within_categories_dir(path: str, module_dir: str) -> bool:
    resolved = os.path.realpath(path)
    allowed = os.path.realpath(module_dir)
    try:
        return os.path.commonpath([resolved, allowed]) == allowed
    except ValueError:
        return False

# assert within_categories_dir(category_file, pkg_dir), f"{category_file} escapes the categories dir"

Prevention

When it happens

Trigger: A policy template or config sets category_file to an absolute path outside the package (e.g. /etc/policies/cat.yaml), a relative path containing ../ traversal, or a path inside the module dir that is a symlink pointing elsewhere — and LITELLM_CONTENT_FILTER_ALLOW_EXTERNAL_PATHS is unset.

Common situations: Docker deployments mounting custom category YAMLs to a path outside the package and referencing them absolutely; penetration tests probing traversal; relative paths that stop resolving inside the module dir once production CWD differs from the project root.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/89d379d3747922cc. Report an issue: GitHub.