{"record":{"id":"89d379d3747922cc","repo":"BerriAI/litellm","slug":"category-file-path-path-is-outside-the-allowed-89d379","errorCode":null,"errorMessage":"Category file path '{path}' is outside the allowed categories directory '{categories_dir}'","messagePattern":"Category file path '(.+?)' is outside the allowed categories directory '(.+?)'","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py","lineNumber":355,"sourceCode":"        return {\n            \"category\": cat_config.get(\"category\"),\n            \"enabled\": cat_config.get(\"enabled\", True),\n            \"action\": cat_config.get(\"action\"),\n            \"category_file\": cat_config.get(\"category_file\"),\n        }\n\n    @staticmethod\n    def _assert_within_categories_dir(path: str, categories_dir: str) -> None:\n        \"\"\"Raise ValueError if path escapes the categories directory.\"\"\"\n        resolved: Final = os.path.realpath(path)\n        allowed: Final = os.path.realpath(categories_dir)\n        try:\n            common: Final = os.path.commonpath([resolved, allowed])\n        except ValueError:\n            # commonpath() raises ValueError on Windows when paths span different drives\n            raise ValueError(f\"Category file path '{path}' is outside the allowed categories directory\")\n        if common != allowed:\n            raise ValueError(\n                f\"Category file path '{path}' is outside the allowed categories directory '{categories_dir}'\"\n            )\n\n    def _resolve_category_file_path(self, file_path: str) -> str:\n        \"\"\"\n        Resolve a category file path that may be relative.\n\n        Paths in policy templates (e.g. category_file) are often stored as\n        relative paths like \"litellm/proxy/.../policy_templates/file.yaml\".\n        These only work when the CWD is the project root. In production\n        (Docker, installed packages, etc.) the CWD is different, so the\n        file isn't found.\n\n        Resolution order:\n        1. Return as-is if absolute or already exists (jailed to module dir).\n        2. Try joining the full path relative to this module's directory (jailed).\n        3. Progressively strip leading path components and try each suffix\n           relative to this module's directory (jailed).","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py#L337-L373","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Place custom category files inside the litellm_content_filter module directory (a subdirectory of it is fine) and reference them with a relative path.","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.","Fix the category_file value: remove ../ segments and absolute paths pointing outside the categories dir.","Replace symlinks that point outside the module dir with real files inside it."],"exampleFix":"# before\ncategory_file: '../../../../etc/custom_policies/secret_category.yaml'\n\n# after — file shipped inside the module dir\ncategory_file: 'categories/secret_category.yaml'","handlingStrategy":"validation","validationCode":"# Pre-flight the same jail the guardrail applies: realpath containment\nimport os\n\ndef within_categories_dir(path: str, module_dir: str) -> bool:\n    resolved = os.path.realpath(path)\n    allowed = os.path.realpath(module_dir)\n    try:\n        return os.path.commonpath([resolved, allowed]) == allowed\n    except ValueError:\n        return False\n\n# assert within_categories_dir(category_file, pkg_dir), f\"{category_file} escapes the categories dir\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Ship custom category YAMLs inside the litellm_content_filter module directory and use relative paths.","Never build category_file from user input — the jail exists precisely to stop traversal reads.","If you must use mounted volumes, document LITELLM_CONTENT_FILTER_ALLOW_EXTERNAL_PATHS=true as an explicit, trusted-env trade-off.","Avoid symlinks inside the module dir that point outside — realpath resolution defeats them silently here as errors."],"tags":["security","path-traversal","filesystem","content-filter"],"backgroundTag":"path-outside-base-directory","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}