Repository navigation
fix(ui): credentials.html XSS sinks + harden XSS guards (follow-up to #781) - #782
Conversation
Address bot review on #781: - credentials.html: the file built HTML by raw string concatenation with no escaper at all (so an escapeHtml grep missed it). Migrate both credential tables to DOM construction: * cached-credentials: key_hash inline-onclick + title="" attr + cell text * server-credentials: name into a data-name="" attr via a quote-unsafe escaper (escHtml), gated only by a server-side regex Also convert the config/credentials/server error-message innerHTML sinks (e.message is unconstrained server text) to textContent. Removed escHtml. - Widen the _INLINE_HANDLER_JS_STRING regression guard: it previously stopped at the first quote and only caught ${...}; now also catches the string-concat onclick shape and multi-arg forms. Documented its exact coverage and accepted gaps rather than over-claiming. Added a self-test for the concat shape and a credentials.html-specific guard. - Add context-limit header comments to the hardened escapeHtml helpers (conversation_live.js, diff_viewer.html): safe for HTML text and *quoted* attributes only, NOT JS-string / URL / unquoted-attribute contexts. - history_list.html empty-state: innerHTML string-literal -> replaceChildren. CSP backstop tracked as a follow-up Trello card (separate refactor; needs nonces or inline-script extraction). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PR Review — fix(ui): credentials.html XSS sinks + harden XSS guardsSolid follow-up to #781. The DOM-construction migration is correct, the comments explain why (not just what), and widening A few items worth considering: 1.
|
Address #782 review round 2 — stop the whack-a-mole with a full sweep of src/luthien_proxy/static/** rather than patching one more file. New attacker-reachable sinks fixed: - activity_monitor.js (the important one): the activity stream is derived from live request traffic. createEventElement rendered the full event via JSON.stringify(event) into a <pre> with NO escaping (JSON.stringify does not escape <,> so a payload field like "</pre><script>" injected script), plus event_type as HTML text. rebuildFilterDropdown interpolated request-derived category/subtype into innerHTML incl. id=""/value="" attrs. All migrated to DOM construction (textContent/dataset/property assignment). - config_dashboard.html: hardened the quote-unsafe esc() (used in value="", title="", data-field="") to escape all five chars; converted the e.message->innerHTML catch sink to textContent. - diff_viewer.html: the two error.message->innerHTML catch handlers (missed in the first pass) -> textContent. Consistency (static literals, not sinks): credentials.html spinner and activity_monitor.js clear-events empty-state -> replaceChildren. Test-guard hardening (review items #2/#4): - credentials guard: replace the brittle whole-file "onclick=" substring match with a scoped _INLINE_HANDLER_ATTR regex (real attribute usage only). - _INLINE_HANDLER_JS_STRING: add a TODO documenting the still-uncaught constant-first-arg/dynamic-second-arg shape. - Add config_dashboard.html + activity_monitor.js to DOM_MIGRATED_FILES; generalize the escaper test to cover esc() as well as escapeHtml. Reviewed-and-safe (documented in the test-module docstring, not migrated): policy_config.js/form_renderer.js (quote-correct esc(), only internally-computed schema paths in inline handlers — deferred like CSP), nav.js (full DOM), client_setup.html (server-side html_escape), index.html / *.html shells. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed in
Test-guard items:
Deferred (tracked): CSP backstop on the admin UI routes — separate refactor (needs nonces or inline-script extraction), Trello card https://trello.com/c/B8wobuI2. The deferred No JS runtime here, so these guards are source-text assertions, not live DOM tests — honest limitation. Posted by Claude Code (Opus 4.8 (1M context)) |
Code Review — PR #782Thorough, well-scoped follow-up to #781. The DOM-construction migration is consistent across files, the regex hardening is honest about its gaps, and the new tests pin the specific shapes that bit this codebase. A few notes below; nothing blocking. Code quality & correctness
Regression-guard hardening
Minor / non-blocking
Security
Test coverageStrong. New parametrized tests cover the three additional DOM-migrated files ( One small suggestion: Overall: ship it. The PR is doing exactly what a security follow-up should — close the gap the original sweep missed, harden the detection that should have caught it, and document the residual gaps rather than over-claim. |
Follow-up to #781 (admin UI stored-XSS class fix), which merged before this bot-review round was addressed. Same concern, same DOM-construction approach; these are the review items that landed after #781 merged.
What this adds
credentials.html— the real gap #781 missedThe bot correctly flagged that
credentials.htmlhad the exact stored-XSS class #781 eliminates, but #781's sweep missed it because the file built HTML by raw string concatenation with noescapeHtmlat all — so a grep forescapeHtml(never surfaced it. Migrated both credential tables to DOM construction:onclick="invalidateOne('" + cred.key_hash + "')"(inline-onclick concat) →addEventListener+dataset.keyHash;key_hashalso flowed into atitle="..."attribute and cell text unescaped →textContent.key_hashis SHA-256 hex (safe by character set today), but that's exactly the "internally derived so it's fine" assumption that rotted in feat: user-differentiation core (history per-user view, labels, session_summaries) #780/feat(ui): cursor pagination + lazy loading for admin dashboard #752 — migrated for real rather than carved out.nameflowed into adata-name="..."attribute via a quote-unsafe escaper (escHtml), gated only by a server-side regex → DOM construction withdataset.name; the existing delegated click listener readsdata-nameunchanged. Removed the now-unusedescHtml.catchblocks injectede.messageintoinnerHTML.e.messageis unconstrained server-returned text (not character-set-limited likekey_hash), so these are genuine sinks →textContent.Regression-guard hardening
_INLINE_HANDLER_JS_STRING: it previously used[^"']*, which stopped at the first quote — so it only caught${...}template-literal shapes and missed the string-concat form (onclick="f('" + x + "')") thatcredentials.htmlactually used, plus multi-arg shapes. Now catches both, with its exact coverage and accepted gaps documented in the comment rather than over-claimed (it flags the dangerous opening idiom; it does not parse JS or prove attacker-influence). Added a self-test for the concat shape and acredentials.html-specific guard.escapeHtmlhelpers (conversation_live.js,diff_viewer.html): safe for HTML text and quoted attributes only — NOT JS-string, URL/javascript:, or unquoted-attribute contexts (use DOM construction there). Prevents the next maintainer from reaching for it in anonclick/hrefsink.history_list.htmlempty-state:innerHTML='<div...>'string literal →replaceChildren(consistency nit; trivial).Deferred (tracked, not in this PR)
<script>blocks and inline Alpine handlers, so a meaningfulscript-src 'self'CSP needs per-page nonces or extracting all inline JS to files.Out of scope (unchanged from #781's reasoning)
policy_config.js/form_renderer.jsinlineonclickhandlers interpolatesafePath(via a quote-escapingesc()) andpath— an internally-computed JSON-schema field path, not request/attacker data. Left as-is, consistent with #781../scripts/dev_checks.shgreen (ruff, pyright 0 errors, full unit suite incl. the 13 XSS guards).🤖 Generated with Claude Code