Fix stored XSS via hapi_url injection in TTYD proxy - #3
Conversation
Sanitize URL read from /home/hapi/url before rendering into HTML: - Validate URL scheme (allow only http/https) to block javascript: URIs - HTML-escape URL and username before template substitution Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @axisrow's task in 52s —— View job Code Review ✅Reviewing the stored XSS fix in the TTYD proxy... Todo List
Security ReviewExcellent security fix! This PR effectively addresses the stored XSS vulnerability with multiple layers of defense. Here's my detailed analysis: ✅ Strengths
🔍 Edge Cases ConsideredThe fix handles these attack vectors correctly:
💡 Minor Observations
🎯 Validation StepsThe provided validation steps in the PR description are comprehensive and test the key attack vectors:
📋 Final VerdictLGTM (Looks Good To Me) - This is a textbook example of how to fix stored XSS:
The fix is production-ready and effectively mitigates the stored XSS vulnerability. |
) Six P1 correctness/reliability fixes from the #101 code review. #3 (proxy.py): enable SO_KEEPALIVE on both tunnel sockets so a client that vanishes without a FIN (laptop sleep, NAT timeout) is detected by the OS and the idle tunnel thread + sockets are freed instead of leaking forever. #4 (ratelimit.py): use time.monotonic() instead of wall-clock time.time(), so a backward clock jump (NTP correction, suspend/resume, VM migration) cannot freeze the window and lock out legitimate users past 60s/300s. #5 (app.py): _check_auth now honours redirect=True in the removed-user branch — a browser navigation with a valid cookie whose account was deleted/renamed lands on /login (302) instead of a raw 403 JSON blob. #6 (proxy.py): add "cookie" to HOP_BY_HOP_HEADERS so the client's signed ttyd_session + csrf_token is not forwarded to the internal ttyd (defense depth). #10 (bin/clihost-sync.sh): the remote symlink guards were byte-identical triplicates (local pair + one copy in each of two `bash -s` heredocs), so hardening one silently bypassed the others — the class of bug caught 3× before (#88/#90/#95). Define them once in a shared emit_remote_prelude injected into both remote payloads. #11 (.env.example): document SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE (read by code + CLAUDE.md but missing from the example), plus the REQUEST_TIMEOUT slowloris knob for completeness. New/changed env vars documented in .env.example: SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE, REQUEST_TIMEOUT. No port or volume changes. Closes #101 findings #3, #4, #5, #6, #10, #11. Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn Co-authored-by: axisrow <axisrow@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
💪 What
app/ttyd_proxy.py)http://andhttps://are allowed for the hapi URL read from/home/hapi/urlhapi_url(withquote=True) before interpolation into the<a href="...">attributeusernamebefore template substitution as defense-in-depth🤔 Why
/home/hapi/urlwere interpolated directly into HTML without any sanitization"></a><script>...) orjavascript:URI schemescript-src 'unsafe-inline', so injected scripts execute in the context of any authenticated user visiting the menu page👩🔬 How to validate
./build.sh && docker run -p 8080:8080 clihosthttp://localhost:8080— the HAPI Server link should render as "not available" (scheme rejected)"><script>...) and no alert fires🤖 Generated with Claude Code