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:
@@ -334,7 +334,15 @@ def handle_post(handler, parsed):
|
|||||||
# ── GET route helpers ─────────────────────────────────────────────────────────
|
# ── GET route helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _serve_static(handler, parsed):
|
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():
|
if not static_file.exists() or not static_file.is_file():
|
||||||
return j(handler, {'error': 'not found'}, status=404)
|
return j(handler, {'error': 'not found'}, status=404)
|
||||||
ext = static_file.suffix.lower()
|
ext = static_file.suffix.lower()
|
||||||
|
|||||||
@@ -250,21 +250,21 @@
|
|||||||
.msg-body{padding-left:0;max-width:100%;}
|
.msg-body{padding-left:0;max-width:100%;}
|
||||||
.msg-role{font-size:12px;}
|
.msg-role{font-size:12px;}
|
||||||
/* Composer */
|
/* Composer */
|
||||||
.composer-wrap{padding:8px 10px 12px;}
|
.composer-wrap{padding:8px 10px 12px!important;}
|
||||||
.composer-box{border-radius:12px;}
|
.composer-box{border-radius:12px;}
|
||||||
.composer-box textarea{font-size:16px;min-height:40px;}
|
.composer-box textarea{font-size:16px;min-height:40px;}
|
||||||
.send-btn{padding:6px 14px;font-size:13px;}
|
.send-btn{padding:6px 14px;font-size:13px;}
|
||||||
/* Empty state */
|
/* Empty state */
|
||||||
.empty-state h2{font-size:18px;}
|
.empty-state h2{font-size:18px;}
|
||||||
.empty-state p{font-size:13px;}
|
.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;}
|
.suggestion-btn{font-size:12px;padding:8px 10px;}
|
||||||
/* Approval card */
|
/* Approval card */
|
||||||
.approval-card{padding:0 10px 8px;}
|
.approval-card{padding:0 10px 8px;}
|
||||||
.approval-btns{gap:6px;}
|
.approval-btns{gap:6px;}
|
||||||
.approval-btn{padding:5px 10px;font-size:11px;}
|
.approval-btn{padding:5px 10px;font-size:11px;}
|
||||||
/* Tool cards */
|
/* Tool cards */
|
||||||
.tool-card{margin-left:0;font-size:12px;}
|
.tool-card{margin-left:0!important;font-size:12px;}
|
||||||
/* Settings modal */
|
/* Settings modal */
|
||||||
.settings-panel{width:95vw;max-width:95vw;}
|
.settings-panel{width:95vw;max-width:95vw;}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user