0.2.41: contain SPA static serving to the web root
Percent-encoded traversal (..%2f) survived routing and let an unauthenticated request read files outside static/, including the database and session secret on the data volume. Paths are now resolved and contained to the frontend build directory; anything that escapes falls back to index.html.
This commit is contained in:
@@ -57,6 +57,19 @@ def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
def _contained_static_path(static_root: Path, path: str) -> Path | None:
|
||||
"""Resolve `path` under `static_root`, returning the file only if it stays
|
||||
within the root. Percent-encoded traversal (..%2f) survives routing and
|
||||
would otherwise let an unauthenticated caller read files outside static/
|
||||
(e.g. the DB or session secret on the data volume). Returns None if the
|
||||
resolved path escapes the root."""
|
||||
root = static_root.resolve()
|
||||
candidate = (root / path).resolve()
|
||||
if candidate != root and root not in candidate.parents:
|
||||
return None
|
||||
return candidate
|
||||
|
||||
|
||||
# Serve built frontend in production (when static/ dir exists next to the app)
|
||||
_static_dir = Path(__file__).resolve().parent.parent / "static"
|
||||
if _static_dir.is_dir():
|
||||
@@ -72,7 +85,9 @@ if _static_dir.is_dir():
|
||||
|
||||
@app.api_route("/{path:path}", methods=["GET", "HEAD"])
|
||||
async def serve_spa(path: str):
|
||||
file = _static_dir / path
|
||||
file = _contained_static_path(_static_dir, path)
|
||||
if file is None:
|
||||
return _index()
|
||||
if file.is_file():
|
||||
# Don't let the HTML entrypoint get cached; fingerprinted assets can cache.
|
||||
if file.name == "index.html":
|
||||
|
||||
Reference in New Issue
Block a user