Remove unsafe-inline from CSP script-src by externalising template JS #49

Merged
coding-agent-marvin8 merged 0 commits from refs/pull/49/head into main 2026-06-15 20:29:46 +00:00
coding-agent-marvin8 commented 2026-06-15 04:35:53 +00:00 (Migrated from codeberg.org)

Closes #48

What changed

All UI JavaScript previously lived in inline <script> blocks across 6 Jinja2 templates (dashboard.html, hashtag_streams.html, topical_tags.html, queue_preview.html, settings.html, stats.html). This forced 'unsafe-inline' in Content-Security-Policy: script-src, which neutralises most XSS protection.

Each inline block has been extracted to a dedicated file under src/fenliu/static/assets/ and the templates updated accordingly. 'unsafe-inline' is now removed from script-src.

How template variables were handled

  • {{ api_key }} (5 templates) — replaced with <meta name="fenliu-api-key" content="...">" in <head>; external JS reads it via document.querySelector`. Meta tags are not executable so CSP does not apply.
  • Chart data (stats.html) — replaced with a <script type="application/json" id="chart-data"> JSON island. type=application/json marks it as non-executable data; CSP ignores it.
  • Inline onclick/onchange/onsubmit attributes — converted to data-* attributes (for template-variable cases) or addEventListener calls in the external JS files.
  • Dynamically-generated HTML with onclick attributes in settings.js and queue-preview.js — converted to data-* attributes with event delegation.

CSP changes

  • script-src: removed 'unsafe-inline'; added https://cdn.jsdelivr.net (Chart.js CDN, already in use by stats.html but missing from the old CSP)
  • style-src: unchanged — PyView still injects per-view inline <style> blocks
Closes #48 ## What changed All UI JavaScript previously lived in inline `<script>` blocks across 6 Jinja2 templates (`dashboard.html`, `hashtag_streams.html`, `topical_tags.html`, `queue_preview.html`, `settings.html`, `stats.html`). This forced `'unsafe-inline'` in `Content-Security-Policy: script-src`, which neutralises most XSS protection. Each inline block has been extracted to a dedicated file under `src/fenliu/static/assets/` and the templates updated accordingly. `'unsafe-inline'` is now removed from `script-src`. ## How template variables were handled - **`{{ api_key }}`** (5 templates) — replaced with `<meta name="fenliu-api-key" content="...">" in `<head>`; external JS reads it via `document.querySelector`. Meta tags are not executable so CSP does not apply. - **Chart data** (`stats.html`) — replaced with a `<script type="application/json" id="chart-data">` JSON island. `type=application/json` marks it as non-executable data; CSP ignores it. - **Inline `onclick`/`onchange`/`onsubmit` attributes** — converted to `data-*` attributes (for template-variable cases) or `addEventListener` calls in the external JS files. - **Dynamically-generated HTML** with `onclick` attributes in `settings.js` and `queue-preview.js` — converted to `data-*` attributes with event delegation. ## CSP changes - `script-src`: removed `'unsafe-inline'`; added `https://cdn.jsdelivr.net` (Chart.js CDN, already in use by `stats.html` but missing from the old CSP) - `style-src`: unchanged — PyView still injects per-view inline `<style>` blocks
coding-agent-marvin8 commented 2026-06-15 12:04:18 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/dashboard.js line 1@marvin8

A07: Identification and Authentication Failures – The API key is stored in a meta tag…

🔴 Not addressing in this PR. The API key was previously embedded directly inside inline <script> blocks (let apiKey = "{{ api_key }}"), so it was already equally readable from the DOM. A <meta> tag does not increase its exposure. Moving to HttpOnly cookies would require a significant auth-flow refactor and is out of scope here. The XSS mitigation delivered by this PR is the removal of 'unsafe-inline' from script-src, which prevents injected scripts from running.

[`packages/fenliu/src/fenliu/static/assets/dashboard.js` line 1](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487977) — @marvin8 > A07: Identification and Authentication Failures – The API key is stored in a meta tag… 🔴 Not addressing in this PR. The API key was previously embedded directly inside inline `<script>` blocks (`let apiKey = "{{ api_key }}"`), so it was already equally readable from the DOM. A `<meta>` tag does not increase its exposure. Moving to HttpOnly cookies would require a significant auth-flow refactor and is out of scope here. The XSS mitigation delivered by this PR is the removal of `'unsafe-inline'` from `script-src`, which prevents injected scripts from running.
coding-agent-marvin8 commented 2026-06-15 12:04:26 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/queue-preview.js line 1@marvin8

A07: Identification and Authentication Failures – The API key is stored in a meta tag…

🔴 Same finding as on dashboard.js — see reply there. The meta tag does not increase the key's DOM visibility relative to its previous embedding inside <script> blocks. Not addressing in this PR.

[`packages/fenliu/src/fenliu/static/assets/queue-preview.js` line 1](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487989) — @marvin8 > A07: Identification and Authentication Failures – The API key is stored in a meta tag… 🔴 Same finding as on `dashboard.js` — see reply there. The meta tag does not increase the key's DOM visibility relative to its previous embedding inside `<script>` blocks. Not addressing in this PR.
coding-agent-marvin8 commented 2026-06-15 12:04:33 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/topical-tags.js line 1@marvin8

A07: Identification and Authentication Failures – The API key is stored in a meta tag…

🔴 Same finding as on dashboard.js — see reply there. Not addressing in this PR.

[`packages/fenliu/src/fenliu/static/assets/topical-tags.js` line 1](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487998) — @marvin8 > A07: Identification and Authentication Failures – The API key is stored in a meta tag… 🔴 Same finding as on `dashboard.js` — see reply there. Not addressing in this PR.
coding-agent-marvin8 commented 2026-06-15 12:04:40 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/stats.js line 2@marvin8

A03: Injection – JSON.parse is called on raw textContent from a DOM element ('chart-data')…

🔴 False positive. <script type="application/json" id="chart-data"> is rendered exclusively by the Jinja2 template via | tojson on Python lists (aggregated post counts and hashtag labels from the database). It carries no user-supplied input and is not attacker-controllable. JSON.parse on server-rendered JSON data is standard practice and is not an injection vector.

[`packages/fenliu/src/fenliu/static/assets/stats.js` line 2](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487995) — @marvin8 > A03: Injection – JSON.parse is called on raw textContent from a DOM element ('chart-data')… 🔴 False positive. `<script type="application/json" id="chart-data">` is rendered exclusively by the Jinja2 template via `| tojson` on Python lists (aggregated post counts and hashtag labels from the database). It carries no user-supplied input and is not attacker-controllable. `JSON.parse` on server-rendered JSON data is standard practice and is not an injection vector.
coding-agent-marvin8 commented 2026-06-15 12:04:47 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/stats.js line 2@marvin8

JSON.parse of document.getElementById('chart-data').textContent assumes the element exists…

🔴 Not addressing. stats.js is loaded exclusively from stats.html, which renders <script id="chart-data"> unconditionally. The element is structurally guaranteed by the template. A missing element would be immediately visible during development; defensive null-guarding for elements that are coupled to the loading template adds noise without real benefit.

[`packages/fenliu/src/fenliu/static/assets/stats.js` line 2](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487965) — @marvin8 > JSON.parse of document.getElementById('chart-data').textContent assumes the element exists… 🔴 Not addressing. `stats.js` is loaded exclusively from `stats.html`, which renders `<script id="chart-data">` unconditionally. The element is structurally guaranteed by the template. A missing element would be immediately visible during development; defensive null-guarding for elements that are coupled to the loading template adds noise without real benefit.
coding-agent-marvin8 commented 2026-06-15 12:04:54 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/queue-preview.js line 279@marvin8

A03: Injection – Unsafe use of JSON.parse on attacker-controlled data from a data attribute…

🔴 False positive. The data-attachments attribute is rendered by Jinja2 with | tojson | replace("'", "&#39;") from database-backed MediaAttachment objects fetched from the Fediverse API. It is entirely server-generated and not user-supplied in any direct sense. Parsing trusted server data with JSON.parse is standard practice.

[`packages/fenliu/src/fenliu/static/assets/queue-preview.js` line 279](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487992) — @marvin8 > A03: Injection – Unsafe use of JSON.parse on attacker-controlled data from a data attribute… 🔴 False positive. The `data-attachments` attribute is rendered by Jinja2 with `| tojson | replace("'", "&#39;")` from database-backed `MediaAttachment` objects fetched from the Fediverse API. It is entirely server-generated and not user-supplied in any direct sense. Parsing trusted server data with `JSON.parse` is standard practice.
coding-agent-marvin8 commented 2026-06-15 12:09:26 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/middleware.py line 111@marvin8

A05: Security Misconfiguration – Adding 'https://cdn.jsdelivr.net' to script-src without verifying…

🔴 Valid concern, not addressing in this PR. The CDN-without-SRI approach matches the pre-existing practice for Tailwind CSS (cdn.tailwindcss.com) and Font Awesome (cdnjs.cloudflare.com), both already present before this PR. Addressing SRI consistently across all three CDN assets — pinning to specific versions and tracking hash updates — is the right scope for a dedicated follow-up issue.

[`packages/fenliu/src/fenliu/middleware.py` line 111](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487974) — @marvin8 > A05: Security Misconfiguration – Adding 'https://cdn.jsdelivr.net' to script-src without verifying… 🔴 Valid concern, not addressing in this PR. The CDN-without-SRI approach matches the pre-existing practice for Tailwind CSS (`cdn.tailwindcss.com`) and Font Awesome (`cdnjs.cloudflare.com`), both already present before this PR. Addressing SRI consistently across all three CDN assets — pinning to specific versions and tracking hash updates — is the right scope for a dedicated follow-up issue.
coding-agent-marvin8 commented 2026-06-15 12:09:32 +00:00 (Migrated from codeberg.org)

packages/fenliu/TODO.md line 4@marvin8

The line is extremely long (over 300 characters)…

🔴 Pre-existing, not introduced by this PR. The only change made to that line was replacing [ ] with [x]. The long description was written when the task was originally added to the TODO list.

[`packages/fenliu/TODO.md` line 4](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487971) — @marvin8 > The line is extremely long (over 300 characters)… 🔴 Pre-existing, not introduced by this PR. The only change made to that line was replacing `[ ]` with `[x]`. The long description was written when the task was originally added to the TODO list.
coding-agent-marvin8 commented 2026-06-15 12:09:39 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/queue-preview.js line 249@marvin8

The keydown event listener registered at line 249 is never removed when the modal is closed…

Valid catch — the Escape-key path and the close-button path both called modal.remove() without first removing the keydown listener; only the backdrop-click path cleaned it up correctly. Pre-existing bug in the original inline <script> that was faithfully extracted. Fixed in commit 59565fb: all teardown paths now go through a single teardown() function backed by an AbortController, so controller.abort() removes every listener atomically regardless of how the modal is closed.

[`packages/fenliu/src/fenliu/static/assets/queue-preview.js` line 249](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487953) — @marvin8 > The keydown event listener registered at line 249 is never removed when the modal is closed… ✅ Valid catch — the Escape-key path and the close-button path both called `modal.remove()` without first removing the `keydown` listener; only the backdrop-click path cleaned it up correctly. Pre-existing bug in the original inline `<script>` that was faithfully extracted. Fixed in commit `59565fb`: all teardown paths now go through a single `teardown()` function backed by an `AbortController`, so `controller.abort()` removes every listener atomically regardless of how the modal is closed.
coding-agent-marvin8 commented 2026-06-15 12:14:28 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/queue-preview.js line 60@marvin8

showFlash uses colours[type] without fallback. If type is not one of the defined keys…

🔴 Not addressing. showFlash has type = "success" as its default and is called only within this file with values "success", "error", "warning", "info" — the complete set of defined keys. There is no call path that can supply an unknown type, so a defensive fallback would add dead code.

[`packages/fenliu/src/fenliu/static/assets/queue-preview.js` line 60](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487959) — @marvin8 > showFlash uses colours[type] without fallback. If type is not one of the defined keys… 🔴 Not addressing. `showFlash` has `type = "success"` as its default and is called only within this file with values `"success"`, `"error"`, `"warning"`, `"info"` — the complete set of defined keys. There is no call path that can supply an unknown type, so a defensive fallback would add dead code.
coding-agent-marvin8 commented 2026-06-15 12:14:35 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/queue-preview.js line 283@marvin8

Direct call to document.getElementById('statusFilter') assumes the element exists…

🔴 Not addressing. queue-preview.js is loaded exclusively from queue_preview.html via <script src="/static/assets/queue-preview.js" defer>. Every element referenced at the bottom of the file (statusFilter, sortBy, sortOrder, cleanupBtn, trimBtn) is always present in that template. The script cannot execute without the template that guarantees their presence.

[`packages/fenliu/src/fenliu/static/assets/queue-preview.js` line 283](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487962) — @marvin8 > Direct call to document.getElementById('statusFilter') assumes the element exists… 🔴 Not addressing. `queue-preview.js` is loaded exclusively from `queue_preview.html` via `<script src="/static/assets/queue-preview.js" defer>`. Every element referenced at the bottom of the file (`statusFilter`, `sortBy`, `sortOrder`, `cleanupBtn`, `trimBtn`) is always present in that template. The script cannot execute without the template that guarantees their presence.
coding-agent-marvin8 commented 2026-06-15 12:14:42 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/dashboard.js line 20@marvin8

showFlash assumes the flash element exists. If document.getElementById('flash') returns null…

🔴 Not addressing. dashboard.js is loaded exclusively from dashboard.html, which unconditionally renders <div id="flash" ...>. The element is structurally guaranteed by the template that loads the script; null-guarding it adds noise.

[`packages/fenliu/src/fenliu/static/assets/dashboard.js` line 20](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487956) — @marvin8 > showFlash assumes the flash element exists. If document.getElementById('flash') returns null… 🔴 Not addressing. `dashboard.js` is loaded exclusively from `dashboard.html`, which unconditionally renders `<div id="flash" ...>`. The element is structurally guaranteed by the template that loads the script; null-guarding it adds noise.
coding-agent-marvin8 commented 2026-06-15 12:14:49 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/dashboard.js line 69@marvin8

formatRelativeTime does not handle future dates gracefully…

🔴 Pre-existing, not introduced by this PR. formatRelativeTime was extracted verbatim from the original inline <script> block in dashboard.html. The behaviour for future timestamps (clock skew, timezone drift) predates this change and is out of scope.

[`packages/fenliu/src/fenliu/static/assets/dashboard.js` line 69](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487968) — @marvin8 > formatRelativeTime does not handle future dates gracefully… 🔴 Pre-existing, not introduced by this PR. `formatRelativeTime` was extracted verbatim from the original inline `<script>` block in `dashboard.html`. The behaviour for future timestamps (clock skew, timezone drift) predates this change and is out of scope.
coding-agent-marvin8 commented 2026-06-15 12:14:56 +00:00 (Migrated from codeberg.org)

packages/fenliu/src/fenliu/static/assets/dashboard.js line 31@marvin8

A04: Insecure Design – Client-side API calls with X-API-Key header can be intercepted…

🔴 Out of scope and pre-existing. The X-API-Key header mechanism is the existing API authentication approach, in place well before this PR. This PR only externalised the JavaScript; it did not alter any API authentication logic. Server-side rate limiting is a server-layer concern unrelated to script placement.

[`packages/fenliu/src/fenliu/static/assets/dashboard.js` line 31](https://codeberg.org/marvinsmastodontools/dujiangyan/pulls/49#issuecomment-17487983) — @marvin8 > A04: Insecure Design – Client-side API calls with X-API-Key header can be intercepted… 🔴 Out of scope and pre-existing. The `X-API-Key` header mechanism is the existing API authentication approach, in place well before this PR. This PR only externalised the JavaScript; it did not alter any API authentication logic. Server-side rate limiting is a server-layer concern unrelated to script placement.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marvin8/dujiangyan!49
No description provided.