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

Abierto Apto para principiantes
#1,112 2 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@0x5t4l1n ya está trabajando en esto.

Desde el 5/10/2026.

  • #1133 de @0x5t4l1n — abierto

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
78/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
flask, python
Área
backend, security

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

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de OWASP/OpenCRE

Todos los issues de OWASP/OpenCRE

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.