PST-094: htmx script-tag and eval surfaces are left at their permissive defaults #823

Closed
opened 2026-10-10 10:57:00 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-094. Severity: Informational.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Informational
  • Category: defence-in-depth / htmx-configuration
  • Location: src/postulo/templates/base.html (27-28)
  • Found by: client-side code
  • Related: PST-041 (#770), PST-093 (#822)

Who can do it, and what they need. Anyone who finds an HTML-injection point in a fragment htmx swaps in (none was found in this audit), on an instance whose operator has relaxed the policy, or on a route whose response is served without it (for example a fragment proxied by nginx that strips headers, or a future csp_override). …

How it works.

  1. The vendored htmx 2.0.10 keeps allowEval: true and allowScriptTags: true; the page's meta config sets only includeIndicatorStyles, historyCacheSize and refreshOnHistoryMiss.
  2. With allowEval, htmx evaluates hx-on:* attribute bodies, hx-vals="js:...", hx-headers="js:...", hx-trigger="click[cond]" filters and hx-request="js:..." through new Function; with allowScriptTags, <script src="/same-origin/..."> elements inside swapped markup are re-created and executed.
  3. Today script-src 'self' with no 'unsafe-eval' makes every one of those throw, and nh3's attribute allowlist (a[href], ol[start] only) and Django autoescaping keep hx-* attributes out of the DOM, so there is no live path.
  4. …

Impact. No impact on a correctly deployed instance. If the policy is ever relaxed (operators sometimes add 'unsafe-eval' for a plugin) or an injection appears in a fragment, the blast radius jumps from markup to script execution in the person's session.

Verification (verdict: confirmed; severity after verification: Informational). File and line are correct for the meta tag (src/postulo/templates/base.html:27-28) and the vendored defaults (src/postulo/static/js/vendor/htmx.min.js). Reachability needs two things at once: an HTML-injection point in a swapped fragment (none found) and either a relaxed CSP or, for script tags, a same-origin attacker-controlled JS file. A fragment response needs no CSP of its own: the loading document's policy governs it. Remediation: add "allowEval":false,"allowScriptTags":false to the htmx-config meta, and consider "allowNestedOobSwaps":false. Check first that no feature depends on swapped-in scripts. Add a test that pins the meta, so the config cannot drift.

Suggested remediation. Add "allowEval": false, "allowScriptTags": false to the htmx-config meta; nothing in the templates uses hx-on, js: prefixes or trigger filters, so this costs nothing and makes htmx refuse those attributes even where the CSP is weakened. Extend tests/test_template_lint.py to fail on hx-on, js:/javascript: prefixes and bracketed hx-trigger conditions, so the setting stays true.

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-094**. Severity: **Informational**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Informational - **Category:** defence-in-depth / htmx-configuration - **Location:** `src/postulo/templates/base.html` (27-28) - **Found by:** client-side code - **Related:** PST-041 (#770), PST-093 (#822) **Who can do it, and what they need.** Anyone who finds an HTML-injection point in a fragment htmx swaps in (none was found in this audit), on an instance whose operator has relaxed the policy, or on a route whose response is served without it (for example a fragment proxied by nginx that strips headers, or a future `csp_override`). … **How it works.** 1. The vendored htmx 2.0.10 keeps `allowEval: true` and `allowScriptTags: true`; the page's meta config sets only `includeIndicatorStyles`, `historyCacheSize` and `refreshOnHistoryMiss`. 2. With `allowEval`, htmx evaluates `hx-on:*` attribute bodies, `hx-vals="js:..."`, `hx-headers="js:..."`, `hx-trigger="click[cond]"` filters and `hx-request="js:..."` through `new Function`; with `allowScriptTags`, `<script src="/same-origin/...">` elements inside swapped markup are re-created and executed. 3. Today `script-src 'self'` with no `'unsafe-eval'` makes every one of those throw, and nh3's attribute allowlist (`a[href]`, `ol[start]` only) and Django autoescaping keep hx-* attributes out of the DOM, so there is no live path. 4. … **Impact.** No impact on a correctly deployed instance. If the policy is ever relaxed (operators sometimes add 'unsafe-eval' for a plugin) or an injection appears in a fragment, the blast radius jumps from markup to script execution in the person's session. **Verification** (verdict: confirmed; severity after verification: Informational). File and line are correct for the meta tag (src/postulo/templates/base.html:27-28) and the vendored defaults (src/postulo/static/js/vendor/htmx.min.js). Reachability needs two things at once: an HTML-injection point in a swapped fragment (none found) and either a relaxed CSP or, for script tags, a same-origin attacker-controlled JS file. A fragment response needs no CSP of its own: the loading document's policy governs it. Remediation: add `"allowEval":false,"allowScriptTags":false` to the htmx-config meta, and consider `"allowNestedOobSwaps":false`. Check first that no feature depends on swapped-in scripts. Add a test that pins the meta, so the config cannot drift. **Suggested remediation.** Add `"allowEval": false, "allowScriptTags": false` to the htmx-config meta; nothing in the templates uses `hx-on`, `js:` prefixes or trigger filters, so this costs nothing and makes htmx refuse those attributes even where the CSP is weakened. Extend tests/test_template_lint.py to fail on `hx-on`, `js:`/`javascript:` prefixes and bracketed `hx-trigger` conditions, so the setting stays true.
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:21 +00:00
Author
Owner

Fixed in 3bbb52734 on main.

Fixed in 3bbb52734 on main.
Sign in to join this conversation.
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
Postulo/postulo#823
No description provided.