Skip to content

fix(proxy): add shared path utilities, prevent directory traversal#25834

Merged
yuneng-berri merged 5 commits intoBerriAI:litellm_yj_apr15from
stuxf:fix/path-traversal-guardrail-yaml
Apr 16, 2026
Merged

fix(proxy): add shared path utilities, prevent directory traversal#25834
yuneng-berri merged 5 commits intoBerriAI:litellm_yj_apr15from
stuxf:fix/path-traversal-guardrail-yaml

Conversation

@stuxf
Copy link
Copy Markdown
Collaborator

@stuxf stuxf commented Apr 16, 2026

Relevant issues

Fixes unsafe path construction in guardrail category endpoint and dotprompt file converter.

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

🐛 Bug Fix

Changes

1. New shared utility: proxy/common_utils/path_utils.py

  • safe_join(base_dir, *parts) — joins path components and verifies the resolved path stays within base_dir. Rejects null bytes and resolves symlinks.
  • safe_filename(filename) — extracts the basename from a user-supplied path, normalizing both Unix and Windows separators. Rejects null bytes and unsafe names.

2. Guardrail category YAML endpoint

GET /guardrails/ui/category_yaml/{category_name} now uses safe_join() instead of raw os.path.join() to construct the category file path. Returns 400 for invalid category names.

3. Dotprompt file converter

POST /utils/dotprompt_json_converter now uses safe_filename() on the uploaded filename before writing to the temp directory.

stuxf added 2 commits April 16, 2026 03:11
Add safe_join() and safe_filename() in proxy/common_utils/path_utils.py
for constructing filesystem paths from user-controlled inputs. Apply to
guardrail category YAML endpoint and dotprompt file converter.
Add null byte rejection to safe_join and safe_filename. Normalize
backslash separators in safe_filename for cross-platform safety.
Include resolved path in ValueError for debugging. Move imports
to module level per project conventions.
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 16, 2026

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

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 16, 2026 3:31am

Request Review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/guardrails/guardrail_endpoints.py 28.57% 5 Missing ⚠️
litellm/proxy/prompts/prompt_endpoints.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 16, 2026

Greptile Summary

This PR introduces safe_join() and safe_filename() in a new path_utils module and applies them to two endpoints that previously used raw os.path.join() on user-supplied input — the guardrail category YAML endpoint and the dotprompt file converter. The fix is correct and well-structured; the path containment logic (realpath + startswith(base + os.sep)) is sound and the null-byte guards are appropriate.

  • A ValueError raised by safe_filename() inside the dotprompt endpoint's broad except Exception handler returns HTTP 500 instead of 400, which misreports a bad-client-input condition as a server error (e.g. a filename like \"\\x00.prompt\" passes the endswith(\".prompt\") pre-check but triggers the null-byte guard inside safe_filename).

Confidence Score: 5/5

Safe to merge; the path traversal fixes are correct and no regressions introduced.

All prior P1 concerns (missing tests, import ordering) have been addressed in follow-up commits. The only remaining finding is a P2 incorrect HTTP status code (500 instead of 400) for a null-byte filename in the dotprompt endpoint — a rejected request with the wrong status code, not a security or data-integrity issue.

litellm/proxy/prompts/prompt_endpoints.py — the ValueError from safe_filename should return 400, not 500.

Important Files Changed

Filename Overview
litellm/proxy/common_utils/path_utils.py New module providing safe_join() and safe_filename(); realpath-based containment and null-byte guards are correctly implemented.
litellm/proxy/guardrails/guardrail_endpoints.py Replaces raw os.path.join with safe_join for category YAML lookup; ValueError is correctly converted to a 400 HTTPException.
litellm/proxy/prompts/prompt_endpoints.py Adds safe_filename() on upload, but ValueError from that call falls through to the catch-all except block, returning HTTP 500 instead of 400 for invalid client-supplied filenames.
tests/test_litellm/proxy/common_utils/test_path_utils.py Covers traversal, null-byte, and safe-name cases for both utilities; single-dot filename is not tested but the production code handles it.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User Request] --> B{Endpoint}
    B --> C["GET /guardrails/ui/category_yaml/{category_name}"]
    B --> D["POST /utils/dotprompt_json_converter"]

    C --> E["os.path.join(__file__, 'categories') → categories_dir (trusted)"]
    E --> F["safe_join(categories_dir, category_name + '.yaml/.json')"]
    F --> G{ValueError?}
    G -- Yes --> H[HTTP 400 Invalid category name]
    G -- No --> I[os.path.exists check]
    I -- Not found --> J[HTTP 404]
    I -- Found --> K[Read & return YAML/JSON]

    D --> L["file.filename.endswith('.prompt') check"]
    L -- No --> M[HTTP 400]
    L -- Yes --> N["safe_filename(file.filename)"]
    N --> O{ValueError?}
    O -- Yes --> P["Caught by except Exception → HTTP 500 (should be 400)"]
    O -- No --> Q["Path(mkdtemp()) / sanitized_name"]
    Q --> R[Write bytes & convert to JSON]
    R --> S[Return JSON result]
Loading

Reviews (2): Last reviewed commit: "style: move import os to module level, f..." | Re-trigger Greptile

from pathlib import Path
from typing import Any, Dict, List, Optional, cast

from litellm.proxy.common_utils.path_utils import safe_filename
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Local import appears before third-party imports

from litellm.proxy.common_utils.path_utils import safe_filename is a local import placed between the stdlib block and the fastapi/pydantic third-party block. PEP 8 and the project's isort config expect the order: stdlib → third-party → local. Ruff will flag this when make lint runs.

Move this line below the pydantic import (after all third-party imports).

Comment on lines +1 to +10
"""
Safe filesystem path construction for user-controlled inputs.

Use safe_join() instead of os.path.join() whenever a path component
comes from user input (request parameters, uploaded filenames, etc.)
to prevent directory traversal attacks.
"""

import os
from pathlib import Path
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 No tests included despite the checklist marking the requirement as done

The pre-submission checklist marks "I have Added testing in the tests/test_litellm/ directory" as completed, but the changeset contains only three source files — no test file for safe_join() or safe_filename(). CLAUDE.md designates this a hard requirement. Basic unit tests should cover at minimum:

  • safe_join blocks ../../ traversal
  • safe_join blocks null-byte injection
  • safe_join allows a valid subdirectory name
  • safe_filename strips directory components (Unix and Windows separators)
  • safe_filename rejects empty/./.. filenames

Comment on lines +1367 to +1368
except ValueError:
raise HTTPException(status_code=400, detail="Invalid category name")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 safe_join called twice when a single traversal check suffices

safe_join calls os.path.realpath twice — once per extension variant — even though only one path will ultimately be opened. The traversal check is the same for both since only the extension differs. Consider validating category_name once (e.g. with safe_filename) before constructing either path, which would make the intent clearer and halve the realpath calls.

This is a style suggestion only; the current approach is functionally correct.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@yuneng-berri yuneng-berri merged commit be1b802 into BerriAI:litellm_yj_apr15 Apr 16, 2026
43 of 44 checks passed
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.

2 participants