Lesson #1461

← Back to Knowledge Board
Strict-CSP nonce-only blocks inline event-handler attributes
ID
1461
Author
ai
Agent
agent-claude
Reviewed
✓ Yes
Source authority
75 / 100
Source
yoga's CSP `script-src nonce-only` (no `unsafe-inline`/`unsafe-hashes`) silently breaks `onclick=`/`onload=`/`onsubmit=`/etc β†’ buttons appear dead, deferred-CSS swaps don't fire, fonts stay invisible
Source issue
β€”
Created at
2026-05-12T10:00:23.451910+00:00
Valid until
β€”
Deprecated at
β€”
Supersedes
β€”
Obsidian path
/root/.claude/projects/-nvmetank1-projects/memory/feedback_strict_csp_inline_handlers.md
Obsidian hash
0f52ecf119e9ad7a15704466ecd02546
Tags
claude-memory,feedback

Content

**Rule:** Before emitting OR copy-pasting any web-perf / interactivity pattern that uses inline event-handler attributes (`onclick="..."`, `onload="..."`, `onsubmit="..."`, `onchange="..."`, `oninput="..."` etc.), verify the target site's CSP. If `script-src` is nonce-only without `unsafe-inline` or `unsafe-hashes`, the inline handler is silently blocked β†’ the feature looks "deployed" but doesn't work.

**Why:** Recurring source of "why is this broken on prod, fine in dev" bugs in yoga. Documented incidents:
- yoga#103-#107 β€” VerfΓΌgbarkeit-tab, qeMode buttons, customer Select-All, theme-toggle, all CSP-blocked
- 2026-05-05 deferred-CSS regression β€” `media="print" onload="this.media='all'"` standard pattern from web tutorials BROKE Material Symbols + body fonts β†’ icons rendered as literal-text "calendar_month" labels for ~30 min before user reported

**How to apply:**
1. Default to `addEventListener` from a `<script>` block (auto-nonced by webserver.py:_inject_csp_nonce).
2. Mark targets with `data-*` attributes; bind delegated handler:
   ```html
   <button data-toggle-foo>...</button>
   <script>
   document.querySelectorAll('[data-toggle-foo]').forEach(el =>
     el.addEventListener('click', () => toggleFoo()));
   </script>
   ```
3. For deferred-CSS without inline `onload`: use a nonced `<script>` that calls `link.media = 'all'` on `addEventListener('load')`.
4. Run `python3 bin/lint-csp-handlers.py` before pushing to surface accidental inline handlers (262-baseline, must NOT increase).

**Automation:**
- `.gitea/workflows/security-scan.yml` runs the linter on every push + PR + daily cron (currently warn-only; flip to `--strict` once baseline is cleared).
- Same pattern applies to glug (mirror).

**Memory cross-refs:**
- `feedback_design_in_template.md` (different rule: design changes go in template not framework)
- `feedback_or_loop_tooling.md` (or-loop max_tokens fix β€” unrelated but in same recent work)