fix(security): sandbox _serve_static() to prevent path traversal

Resolved path was not checked against the static/ directory, allowing
GET /static/../../../../etc/passwd to serve arbitrary files.

Fix: resolve the path and call relative_to(static_root) before serving.
Returns 404 for any path that escapes the static/ directory.

fix(css): add !important to three dead mobile overrides in @media(640px)

Three @media(max-width:640px) rules added by the mobile responsive PR
were silently overridden by later bare rules in the same stylesheet:
  .composer-wrap padding (overridden by line 347)
  .suggestion-grid max-width (overridden by line 364)
  .tool-card margin-left (overridden by line 460)

Fix: add !important to these three declarations so the mobile overrides
actually fire on narrow screens.

Tests: 224 passed, 0 failed.
This commit is contained in:
Nathan Esquenazi
2026-04-02 06:39:18 +00:00
parent 85557381ec
commit 0875dddbff
2 changed files with 12 additions and 4 deletions

View File

@@ -334,7 +334,15 @@ def handle_post(handler, parsed):
# ── GET route helpers ─────────────────────────────────────────────────────────
def _serve_static(handler, parsed):
static_file = Path(__file__).parent.parent / parsed.path.lstrip('/')
static_root = (Path(__file__).parent.parent / 'static').resolve()
# Strip the leading '/static/' prefix and resolve the full path
rel = parsed.path[len('/static/'):]
static_file = (static_root / rel).resolve()
# Sandbox check: resolved path must stay inside static_root
try:
static_file.relative_to(static_root)
except ValueError:
return j(handler, {'error': 'not found'}, status=404)
if not static_file.exists() or not static_file.is_file():
return j(handler, {'error': 'not found'}, status=404)
ext = static_file.suffix.lower()