Skip to content

fix(security): patch 3 critical vulnerabilities (Antiproof report)#24336

Open
Harshit28j wants to merge 3 commits intoBerriAI:mainfrom
Harshit28j:worktree-fix-security-vulns
Open

fix(security): patch 3 critical vulnerabilities (Antiproof report)#24336
Harshit28j wants to merge 3 commits intoBerriAI:mainfrom
Harshit28j:worktree-fix-security-vulns

Conversation

@Harshit28j
Copy link
Collaborator

Summary

Fixes three security vulnerabilities reported by Antiproof (UC Berkeley) on March 7, 2026:

  • Vuln Give me consistent exceptions  #1 — Custom Code Guardrail RCE (Critical): Added PROXY_ADMIN role check to /apply_guardrail endpoint and blocked 12 frame traversal patterns (cr_frame, gi_frame, f_back, f_globals, etc.) in the code validator to prevent sandbox escape.
  • Vuln Enable model / call timeouts #2 — Skills Sandbox Path Traversal (Critical): Sanitized ZIP entry names in extract_all_files() to reject .. and absolute paths, and added realpath containment check in sandbox_executor before writing files to disk.
  • Vuln Guarantee format of exceptions #3 — OIDC Arbitrary File Read (High): Added _validate_oidc_file_path() blocking reads from sensitive directories (/etc/, /root/, /proc/, /sys/, /dev/) and restricted /health/test_connection endpoint to admin users only.

Test plan

  • 16/16 guardrail security tests passing (7 new frame traversal tests)
  • 21/21 secret manager tests passing (8 new OIDC path validation tests)
  • 4/4 skills path traversal tests passing (new test file)
  • Verify PoC reproduction steps from Antiproof reports no longer work on patched build
  • Deploy to staging and confirm no regressions on legitimate OIDC file reads and guardrail usage

🤖 Generated with Claude Code

github-actions bot and others added 2 commits March 18, 2026 15:33
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…/UC Berkeley

1. Custom Code Guardrail RCE: Add PROXY_ADMIN role check to /apply_guardrail
   endpoint and block frame traversal patterns (cr_frame, gi_frame, f_back,
   f_globals, etc.) in the code validator to prevent sandbox escape.

2. Skills Sandbox Path Traversal: Sanitize ZIP entry names in extract_all_files()
   to reject .. and absolute paths, and add realpath containment check in
   sandbox_executor before writing files.

3. OIDC Arbitrary File Read: Add path validation blocking sensitive directories
   (/etc, /root, /proc, /sys, /dev) in the OIDC file provider, and restrict
   /health/test_connection endpoint to admin users only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 22, 2026 2:09am

Request Review

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ Harshit28j
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@codspeed-hq
Copy link
Contributor

codspeed-hq bot commented Mar 22, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing Harshit28j:worktree-fix-security-vulns (71f0b4b) with main (f5194b5)

Open in CodSpeed

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR patches three critical/high security vulnerabilities reported by Antiproof (UC Berkeley): a custom code guardrail RCE via Python frame-traversal, a Skills sandbox ZIP path traversal, and an OIDC arbitrary file read. The intent is sound and the test coverage is strong, but several of the new defences contain their own correctness gaps that could undermine the fixes.

Key changes:

  • code_validator.py: 12 new regex patterns blocking frame-traversal sandbox-escape vectors (cr_frame, gi_frame, f_back, f_globals, etc.)
  • prompt_injection.py: ZIP entry names are now validated for .. components and absolute paths before extraction
  • sandbox_executor.py: os.path.realpath containment check added before writing staged files
  • secret_managers/main.py: _validate_oidc_file_path() denylist added before any open() on OIDC token paths
  • guardrail_endpoints.py / _health_endpoints.py: Both endpoints now require PROXY_ADMIN role

Issues found:

  • The LITELLM_ALLOWED_OIDC_DIRS allowlist in secret_managers/main.py uses resolved.startswith(d) without a trailing path separator, allowing /allowed_dir_evil/token to bypass an allowlist of /allowed_dir
  • _validate_oidc_file_path does not block relative paths (e.g. etc/passwd); the denylist prefixes all begin with / so relative paths pass every check
  • sandbox_executor.py has the same startswith issue without a trailing os.sep, though practically low-risk due to random tmpdir names
  • Both /apply_guardrail and /health/test_connection are now hard-gated to PROXY_ADMIN with no feature flag, which is a backwards-incompatible breaking change for existing non-admin callers per the project's compatibility policy
  • Several co_* code-object attributes useful for sandbox escape (co_code, co_cellvars, co_freevars, co_filename) are not yet in the FORBIDDEN_PATTERNS denylist

Confidence Score: 2/5

  • Not safe to merge as-is — two of the three security fixes contain correctness gaps that could be exploited, and two endpoints have breaking API changes without backwards-compatibility guards.
  • The PR addresses real, confirmed vulnerabilities with good test coverage, but introduces its own security bugs: the LITELLM_ALLOWED_OIDC_DIRS allowlist bypass and the relative-path gap in _validate_oidc_file_path mean Vuln Guarantee format of exceptions #3 is only partially fixed. The sandbox_executor.py startswith issue is a latent logic flaw. Additionally, the unconditional RBAC hardening on /apply_guardrail and /health/test_connection violates the project's backwards-compatibility policy without a feature flag.
  • litellm/secret_managers/main.py (allowlist bypass + relative path gap), litellm/llms/litellm_proxy/skills/sandbox_executor.py (startswith separator), litellm/proxy/guardrails/guardrail_endpoints.py and litellm/proxy/health_endpoints/_health_endpoints.py (breaking RBAC change)

Important Files Changed

Filename Overview
litellm/secret_managers/main.py Adds OIDC file-path validation (_validate_oidc_file_path) with a denylist of sensitive directories, but has two security issues: the LITELLM_ALLOWED_OIDC_DIRS allowlist is bypassable via startswith without a trailing separator, and relative paths are not blocked.
litellm/proxy/guardrails/guardrail_endpoints.py Adds PROXY_ADMIN role check to /apply_guardrail; correctly fixes the RCE vector but is a breaking change for existing non-admin callers without a backwards-compatible feature flag.
litellm/proxy/health_endpoints/_health_endpoints.py Restricts /health/test_connection to PROXY_ADMIN; fixes the OIDC arbitrary-read path but is backwards-incompatible without a feature flag for existing non-admin users.
litellm/llms/litellm_proxy/skills/sandbox_executor.py Adds realpath containment check before writing ZIP-extracted files; the startswith comparison lacks a trailing path separator and is logically unsound, though practically safe due to random tmpdir names.
litellm/llms/litellm_proxy/skills/prompt_injection.py ZIP entry path traversal validation is correctly implemented with both a split-component .. check and a normpath double-check; logic is sound.
litellm/proxy/guardrails/guardrail_hooks/custom_code/code_validator.py 12 new frame-traversal regex patterns added to FORBIDDEN_PATTERNS; blocks major escape vectors but misses several other co_* code-object attributes (co_code, co_cellvars, co_freevars, co_filename).
tests/litellm/proxy/guardrails/test_custom_code_security.py 7 new unit tests for frame-traversal patterns; all are pure mock/unit tests with no network calls, correctly extending existing coverage.
tests/llm_translation/test_skills_security.py New test file covering ZIP path traversal via mocks; no real network calls, correctly validates the extraction logic.
tests/test_litellm/secret_managers/test_secret_managers_main.py 8 new OIDC path validation tests covering blocked system directories and a valid tmp-path round-trip; no network calls; good coverage of the happy/sad paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B{Endpoint?}

    B -->|POST /apply_guardrail| C{PROXY_ADMIN role?}
    C -->|No| D[403 Forbidden]
    C -->|Yes| E[validate_custom_code against FORBIDDEN_PATTERNS]
    E --> F{Pattern match?}
    F -->|Yes| G[CustomCodeValidationError]
    F -->|No| H[Execute Guardrail]

    B -->|POST /health/test_connection| I{PROXY_ADMIN role?}
    I -->|No| J[403 Forbidden]
    I -->|Yes| K[Test Model Connection]

    B -->|OIDC file read via get_secret| L[_validate_oidc_file_path]
    L --> M{Blocked prefix?}
    M -->|Yes| N[ValueError: path blocked]
    M -->|No| O{LITELLM_ALLOWED_OIDC_DIRS set?}
    O -->|Yes| P{startswith allowed_dir?}
    P -->|No| Q[ValueError: not in allowlist]
    P -->|"Yes (bypass risk: no trailing sep)"| R[open and read file]
    O -->|No| R

    B -->|Skill ZIP via extract_all_files| S[Check for .. and abs path]
    S --> T{Path traversal?}
    T -->|Yes| U[Skip entry]
    T -->|No| V[Stage file in tmpdir]
    V --> W{realpath startswith tmpdir?}
    W -->|"No"| X[ValueError: traversal detected]
    W -->|"Yes (no trailing sep)"| Y[Write and copy to sandbox]
Loading

Last reviewed commit: "Merge branch 'main' ..."

Comment on lines +2083 to +2087
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
detail="Admin access required to apply guardrails",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Breaking change without feature flag

Restricting /apply_guardrail to PROXY_ADMIN only is a backwards-incompatible change. Any non-admin users who were legitimately calling this endpoint (e.g. internal tooling, team members testing guardrails) will now get a 403 with no opt-out. Per the project's policy on backwards compatibility, breaking changes should be gated behind a user-controlled flag rather than applied unconditionally.

Consider guarding this behind an env/config flag, e.g.:

if litellm.require_admin_for_apply_guardrail and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
    raise HTTPException(
        status_code=403,
        detail="Admin access required to apply guardrails",
    )

This allows existing deployments to opt-in at their own pace while the default can be True for new installs.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +1499 to +1503
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
raise HTTPException(
status_code=403,
detail="Admin access required to test model connections",
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Breaking change without feature flag

Same backwards-compatibility concern as /apply_guardrail: /health/test_connection was previously accessible to any authenticated user. Restricting it unconditionally to PROXY_ADMIN will break non-admin scripts and CI pipelines that relied on it for health-checking credentials. This should be gated behind a flag per the project's backwards-compatibility policy.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +44 to +50
allowed_dirs_env = os.getenv("LITELLM_ALLOWED_OIDC_DIRS")
if allowed_dirs_env:
allowed = [os.path.realpath(d.strip()) for d in allowed_dirs_env.split(",")]
if not any(resolved.startswith(d) for d in allowed):
raise ValueError(
f"OIDC file path {resolved} not in allowed directories"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 startswith allowlist bypass — missing path separator

resolved.startswith(d) can be fooled when an allowed directory name is a prefix of an attacker-controlled directory name. For example, if LITELLM_ALLOWED_OIDC_DIRS=/var/run/secrets, the path /var/run/secrets.evil/token passes the check because "/var/run/secrets.evil/token".startswith("/var/run/secrets") is True.

Each entry should be normalised to end with / before the comparison:

allowed = [
    (os.path.realpath(d.strip()).rstrip("/") + "/")
    for d in allowed_dirs_env.split(",")
]
if not any(resolved.startswith(d) for d in allowed):
    raise ValueError(
        f"OIDC file path {resolved} not in allowed directories"
    )

Comment on lines +101 to +105
real_path = os.path.realpath(local_path)
if not real_path.startswith(os.path.realpath(tmpdir)):
raise ValueError(
f"Path traversal detected in skill file: {path}"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 startswith containment check missing path separator

real_path.startswith(os.path.realpath(tmpdir)) is logically flawed in the same way as the OIDC allowlist: if tmpdir were /tmp/abc, a resolved path of /tmp/abcdef/evil would pass the check. In practice, tempfile.TemporaryDirectory() generates long random suffixes that make an accidental prefix collision very unlikely, but relying on that is fragile. Append a trailing separator to make the boundary check explicit:

safe_root = os.path.realpath(tmpdir) + os.sep
if not real_path.startswith(safe_root):
    raise ValueError(
        f"Path traversal detected in skill file: {path}"
    )

Comment on lines +23 to +41
def _validate_oidc_file_path(file_path: str) -> None:
"""Validate that an OIDC token file path is safe to read."""
# Normalize the path to resolve .. components
normalized = os.path.normpath(file_path)
resolved = os.path.realpath(file_path)

# Check both normalized and resolved paths against blocked prefixes
# (resolved handles symlinks, normalized handles cases where file doesn't exist yet)
for prefix in _OIDC_BLOCKED_PATH_PREFIXES:
if normalized.startswith(prefix) or resolved.startswith(prefix):
raise ValueError(
f"OIDC file path not allowed: reading from {prefix} is blocked for security"
)
# Also check /private variants (macOS)
private_prefix = f"/private{prefix}"
if resolved.startswith(private_prefix):
raise ValueError(
f"OIDC file path not allowed: reading from {prefix} is blocked for security"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Denylist incomplete — relative paths to sensitive locations not blocked

_validate_oidc_file_path only blocks paths whose normpath/realpath starts with one of the hardcoded prefixes. All of those prefixes begin with /. A relative path such as etc/passwd produces:

  • os.path.normpath("etc/passwd")etc/passwd (no leading /, passes every check)
  • os.path.realpath("etc/passwd")<CWD>/etc/passwd (only blocked if CWD == /)

If the OIDC file path is ultimately sourced from user-supplied input that does not enforce an absolute path, relative paths can bypass the denylist entirely. At minimum, consider rejecting any path that does not start with / after normalization, or resolving the path against a known safe root before checking.

Comment on lines +37 to +48
(r"\.cr_frame", "coroutine frame access is not allowed"),
(r"\.gi_frame", "generator frame access is not allowed"),
(r"\.ag_frame", "async generator frame access is not allowed"),
(r"\.f_back", "frame traversal via f_back is not allowed"),
(r"\.f_globals", "frame globals access is not allowed"),
(r"\.f_locals", "frame locals access is not allowed"),
(r"\.f_code", "frame code access is not allowed"),
(r"\.f_builtins", "frame builtins access is not allowed"),
(r"\bcurrentframe\b", "currentframe() is not allowed"),
(r"\b_getframe\b", "sys._getframe() is not allowed"),
(r"\.co_consts", "code object constants access is not allowed"),
(r"\.co_names", "code object names access is not allowed"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Frame code object attributes incompletely blocked

Several additional co_* attributes are useful for sandbox escape and are not yet blocked:

  • co_code — raw bytecode, can be used to reconstruct and re-execute code
  • co_cellvars / co_freevars — expose closure cell contents
  • co_filename — discloses server file-system paths

Consider extending the list:

(r"\.co_code\b", "code object bytecode access is not allowed"),
(r"\.co_cellvars\b", "code object cell variables access is not allowed"),
(r"\.co_freevars\b", "code object free variables access is not allowed"),
(r"\.co_filename\b", "code object filename access is not allowed"),

Copy link
Contributor

@themavik themavik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid hardening across multiple vectors — ZIP traversal, frame access, OIDC path validation, and RBAC checks.

One gap in the code validator: string-level regex patterns like \.cr_frame can be bypassed with getattr(obj, 'cr_' + 'frame') or vars() tricks. The validator would need AST-level analysis to catch those. Not a blocker since this is defense-in-depth on top of the sandbox, but worth noting.

The LITELLM_ALLOWED_OIDC_DIRS escape hatch is a nice touch — avoids breaking Kubernetes token mounts in /etc/kubernetes/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants