Skip to main content

max / goingson

Wire XSS escaping gate into lint and ratchet inline event handlers lint-frontend.sh now runs the frontend JS suite (including the CHRONIC-XSS escaping-enforcement gate) as a step, so the gate fails the lint literally instead of only when invoked by hand -- avoids the node-per-cargo-build anti-pattern (GO has no CI). The dialog-ban rules now skip js/tests/ so XSS attack payloads stop false-positiving as alert(/confirm( calls. Adds an inline event-handler ratchet (budget 391; the occurrence count may only decrease) to drive the CSP unsafe-inline drawdown without backsliding while handlers are migrated to addEventListener. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-23 19:11 UTC
Commit: 56842abc1d2aeea16117de8d6bf07afc026818c8
Parent: dd878b2
1 file changed, +43 insertions, -1 deletion
@@ -41,7 +41,9 @@ hits=$(grep -rnE 'var\(--[a-z-]+,\s*#' "$FRONTEND" --include='*.js' --include='*
41 41 report "no-var-fallback-hex" "var(--token, #fallback) — drop the fallback; it bypasses themes." "$hits"
42 42
43 43 # 4. No window.confirm / bare confirm() — route through GoingsOn.ui.showConfirmDialog.
44 - hits=$(grep -rnE '\b(window\.)?confirm\(' "$SRC_JS" 2>/dev/null | grep -vE 'showConfirmDialog|confirmDelete|//\s*\*|\*\s' || true)
44 + # Skip js/tests/: test fixtures carry attack payloads and mocks that
45 + # legitimately contain `confirm(`/`alert(` as data, not as app calls.
46 + hits=$(grep -rnE '\b(window\.)?confirm\(' "$SRC_JS" 2>/dev/null | grep -v '/tests/' | grep -vE 'showConfirmDialog|confirmDelete|//\s*\*|\*\s' || true)
45 47 report "no-window-confirm" "window.confirm() — use GoingsOn.ui.showConfirmDialog instead." "$hits"
46 48
47 49 # 5. No inline style= that touches color / background / border / shadow / font / padding values.
@@ -53,10 +55,50 @@ hits=$(grep -rnE 'empty-dashboard-list|kanban-empty|virtual-scroller-empty' "$FR
53 55 report "no-deprecated-empty-states" "Deprecated class — use .empty-state with --compact / --dashboard / --error." "$hits"
54 56
55 57 # 7. No native browser dialogs. Charter rule from Phase 7 roll-up.
58 + # Skip js/tests/ (attack payloads / mocks reference these as data, not calls).
56 59 hits=$(grep -rnE '\b(window\.)?(confirm|prompt|alert)\(' "$SRC_JS" 2>/dev/null \
60 + | grep -v '/tests/' \
57 61 | grep -vE 'showConfirmDialog|showPromptDialog|confirmDelete|//\s|\*\s' || true)
58 62 report "no-native-dialogs" "window.confirm/prompt/alert are banned — use GoingsOn.ui.show{Confirm,Prompt}Dialog or showToast." "$hits"
59 63
64 + # 8. Frontend JS test suite, including the CHRONIC-XSS escaping-enforcement gate
65 + # (js/tests/run.js). GO has no CI and running node inside `cargo build` is an
66 + # anti-pattern, so the gate lives here in the lint script that's run before a
67 + # commit — wiring it in makes the "build-failing gate" literal rather than a
68 + # test you have to remember to invoke. run.js exits non-zero on any failure.
69 + if command -v node >/dev/null 2>&1; then
70 + if ! js_out=$(node "$SRC_JS/tests/run.js" 2>&1); then
71 + echo
72 + echo "[frontend-js-tests] JS test suite failed (includes the CHRONIC-XSS escaping gate):"
73 + echo "$js_out" | tail -40
74 + violations=$((violations + 1))
75 + fi
76 + else
77 + echo "[frontend-js-tests] WARNING: node not found — skipping JS tests and the XSS gate"
78 + fi
79 +
80 + # 9. Inline event-handler ratchet (CSP `unsafe-inline` drawdown).
81 + # Dropping `script-src 'unsafe-inline'` from tauri.conf.json requires
82 + # converting every inline `on<event>="..."` handler to addEventListener — CSP
83 + # has no nonce for inline handlers. That's a phased migration; this gate stops
84 + # BACKSLIDING in the meantime: the occurrence count may only go DOWN. When you
85 + # remove handlers, lower INLINE_HANDLER_BUDGET to the new count (the script
86 + # prints the new number on progress). When it reaches 0 and the two inline
87 + # <script> blocks (index.html, compose.html) are externalized, drop
88 + # 'unsafe-inline' from the CSP. Tests and minified bundles are excluded.
89 + INLINE_HANDLER_BUDGET=391
90 + inline_count=$(grep -rlE "\bon[a-z]+=[\"']" "$FRONTEND" --include='*.html' --include='*.js' 2>/dev/null \
91 + | grep -v '/tests/' | grep -v '\.min\.' \
92 + | tr '\n' '\0' | xargs -0 grep -oE "\bon[a-z]+=[\"']" 2>/dev/null | wc -l | tr -d ' ')
93 + if [ "$inline_count" -gt "$INLINE_HANDLER_BUDGET" ]; then
94 + report "inline-handler-ratchet" \
95 + "Inline event handlers rose to $inline_count (budget $INLINE_HANDLER_BUDGET). Don't add inline on*= attributes; wire events with addEventListener (data-* + delegation)." \
96 + "count $inline_count > budget $INLINE_HANDLER_BUDGET"
97 + elif [ "$inline_count" -lt "$INLINE_HANDLER_BUDGET" ]; then
98 + echo "[inline-handler-ratchet] progress: $inline_count inline handlers (budget $INLINE_HANDLER_BUDGET)."
99 + echo " Lower INLINE_HANDLER_BUDGET in scripts/lint-frontend.sh to $inline_count to lock in the gain."
100 + fi
101 +
60 102 if [ $violations -eq 0 ]; then
61 103 echo "frontend lint: clean"
62 104 exit 0