Information disclosure via filesystem existence oracle in web_main.py catch-all route

Aperta Adatta ai principianti
#1,112 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@0x5t4l1n ci sta già lavorando.

Dal 5/10/2026.

  • #1133 di @0x5t4l1n — aperta

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
78/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
flask, python
Ambito
backend, security

Direzione di ricerca

Inizia da application/web/web_main.py:747-753 e ispeziona la route index catch-all, quindi riproduci le risposte differenti con un client di test Flask usando percorsi esistenti e inesistenti al di fuori della directory statica. Aggiungi un test di regressione che copra questi casi e verifica che entrambi vengano risolti coerentemente nella risposta prevista senza esporre l’esistenza nel filesystem.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

@northdpole as discussed in #project-cre.

Summary

The catch-all route that serves the React app answers differently depending on whether a path exists on the server filesystem, so an unauthenticated caller can test for arbitrary paths outside the static directory. File contents do not leak; existence does.

Where

application/web/web_main.py:747-753 (on fbacf559):

@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def index(path: str) -> Any:
    if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
        return send_from_directory(app.static_folder, path)
    else:
        return send_from_directory(app.static_folder, "index.html")

Why it happens

Two functions decide what is in bounds and they disagree.

os.path.join applies no containment check, and Flask's <path:path> converter passes .. through, so the guard resolves paths outside the static root and os.path.exists answers about them truthfully.

send_from_directory does check, through Werkzeug's safe_join, and refuses anything that escapes, raising 404.

The read is therefore blocked. But the two branches return different responses, and that difference is the leak: an existing path takes the first branch and 404s, a missing one falls through and serves index.html with 200.

Reproduction

Flask 3.1.3, Werkzeug 3.1.8, route copied verbatim into a test client:

GET /../../../../etc/passwd                 404   207 bytes   (exists)
GET /../../../../etc/definitely-not-here    200    16 bytes   (does not exist)
GET /../../../../etc/shadow                 404   207 bytes   (exists, unreadable)
GET /../../../../root                       404   207 bytes   (directory, exists)
GET /../../../../home/nonexistentuser       200    16 bytes   (does not exist)

404 means the path exists, 200 means it does not. It reports existence for files the process cannot read, and for directories.

Impact

Low. No file contents are disclosed and there is no read primitive; safe_join holds. What leaks is filesystem structure, to an unauthenticated remote caller: host fingerprinting, which packages are installed, whether particular users, config files or deployment paths exist. Roughly CWE-204, observable response discrepancy, reached through a path-traversal-shaped input.

Suggested fix

Have both branches use the same containment rule:

from werkzeug.security import safe_join

@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def index(path: str) -> Any:
    if path:
        full = safe_join(app.static_folder, path)
        if full is not None and os.path.exists(full):
            return send_from_directory(app.static_folder, path)
    return send_from_directory(app.static_folder, "index.html")

Happy to open a PR with this and a regression test if useful.

Lingua principale
Python
Stelle
210
Fork
167
Merge medio
4g 12h
PR unite (30g)
28

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di OWASP/OpenCRE

Tutte le issue di OWASP/OpenCRE

Issue simili

Altre issue su Python

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.