Information disclosure via filesystem existence oracle in web_main.py catch-all route
Los mantenedores suelen responder en 1 día
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 78/100
Línea de trabajo
Comienza en application/web/web_main.py:747-753 e inspecciona la ruta de índice catch-all; después, reproduce las respuestas diferentes con un cliente de pruebas de Flask usando rutas existentes e inexistentes fuera del directorio estático. Añade una prueba de regresión que cubra esos casos y verifica que ambas se resuelvan de forma coherente en la respuesta prevista sin exponer la existencia en el sistema de archivos.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
@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.
- Lenguaje dominante
- Python
- Estrellas
- 210
- Forks
- 167
- Merge medio
- 4 d 12 h
- PR fusionados (30 d)
- 28
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- 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 OWASP/OpenCRE
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
OWASP/OpenCRE#1215 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Map Analysis continues polling after a background job failsPosiblemente ocupada @amantebriwal4321 la tomó hace 4 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
OWASP/OpenCRE#1211 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
OSCAL Compass adopterAbierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 66/100
Los mantenedores suelen responder en 1 día
-
MCP tools return "Internal MCP tool error." when OpenCRE is unreachable or times outPosiblemente ocupada @ShaanGS la tomó hace 23 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
OWASP/OpenCRE#1163 · 5 comentarios ·
Los mantenedores suelen responder en 1 día
-
find_node_by_name endpoint does not enforce MAX_ITEMS_PER_PAGE, allowing unbounded pagination queries`Posiblemente ocupada @Jeffy123-zhu la tomó hace 26 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
OWASP/OpenCRE#1149 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de OWASP/OpenCRE
Issues similares
-
Unknown stop ID in slow zone data crashes post formatting and skips the rest of that day's postsAbiertobackend bug tm-new-volunteer
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
transitmatters/mbta-slow-zone-bot#81 · 1 comentario ·
-
area:schema documentation
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
Los mantenedores suelen responder en 1 día
-
risk:high type:ci
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
spacetelescope/rdmt-spire#49 ·
Los mantenedores suelen responder en 1 día