From 0875dddbffbc49f0af34aea525a665c0739d3dd9 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Thu, 2 Apr 2026 06:39:18 +0000 Subject: [PATCH] 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. --- api/routes.py | 10 +++++++++- static/style.css | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/api/routes.py b/api/routes.py index aa3b68f..9f9d276 100644 --- a/api/routes.py +++ b/api/routes.py @@ -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() diff --git a/static/style.css b/static/style.css index 642e97c..ee1d542 100644 --- a/static/style.css +++ b/static/style.css @@ -250,21 +250,21 @@ .msg-body{padding-left:0;max-width:100%;} .msg-role{font-size:12px;} /* Composer */ - .composer-wrap{padding:8px 10px 12px;} + .composer-wrap{padding:8px 10px 12px!important;} .composer-box{border-radius:12px;} .composer-box textarea{font-size:16px;min-height:40px;} .send-btn{padding:6px 14px;font-size:13px;} /* Empty state */ .empty-state h2{font-size:18px;} .empty-state p{font-size:13px;} - .suggestion-grid{max-width:100%;} + .suggestion-grid{max-width:100%!important;} .suggestion-btn{font-size:12px;padding:8px 10px;} /* Approval card */ .approval-card{padding:0 10px 8px;} .approval-btns{gap:6px;} .approval-btn{padding:5px 10px;font-size:11px;} /* Tool cards */ - .tool-card{margin-left:0;font-size:12px;} + .tool-card{margin-left:0!important;font-size:12px;} /* Settings modal */ .settings-panel{width:95vw;max-width:95vw;} }