KB — Building — Security baseline
Security baseline — CSP & XSS researched
The cross-site-scripting defenses every site we build must meet before it ships custom code, embeds, or user input.
Use this when
You are writing any custom JavaScript, accepting user input, embedding third-party scripts, or rendering author-supplied HTML.
Definition of done
(1) No untrusted data is interpolated into <script> tags, event handlers, or eval().
(2) Any author-supplied HTML passes through DOMPurify.
(3) A strict (nonce/hash) Content-Security-Policy is set — deployed in Report-Only first, then enforced.
Requirements
- Lean on the framework's auto-encoding. React/JSX and Django escape interpolated values by default — keep it that way. Audit every escape hatch (
dangerouslySetInnerHTML, AngularbypassSecurityTrustAs*, LitunsafeHTML). - Encode by output context. The correct escaping differs per context — HTML body, attribute, JavaScript, CSS, URL. Never hand-roll this in a context the framework doesn't cover.
- Never interpolate untrusted data into
<script>tags, HTML comments, CSS selectors, event handlers (onclick), oreval()/setTimeout(). Prefer safe sinks:.textContent,.setAttribute()with a hardcoded name,createTextNode(). - Sanitize HTML you must render. When users author HTML (WYSIWYG), encoding breaks it — use DOMPurify:
const clean = DOMPurify.sanitize(dirty); - Set a strict CSP as the backup layer:
Use a per-response nonce (server-rendered) or aContent-Security-Policy: script-src 'nonce-{RANDOM_PER_RESPONSE}' 'strict-dynamic'; object-src 'none'; base-uri 'none';sha256-…hash (static HTML). Deploy asContent-Security-Policy-Report-Onlyfirst, watch reports, then enforce.
Gotchas
Avoid allowlist CSPs. They are easily bypassed and bloat fast — Google Analytics alone can need ~187 allowlisted domains. Strict nonce/hash beats allowlist.
Webflow exports are static — nonce CSP isn't available; use hash-based, and remember setting response headers on Webflow hosting typically needs a Cloudflare proxy in front. On HubSpot the CSP header is a domain-settings toggle (see SSL / domain / DNS cutover).
CSP is a backup, not a substitute for encoding/sanitization. WAFs do not catch DOM-based XSS — code-review custom JS regardless.
Why & sources
No single technique solves XSS — defense must be layered. Full cited write-up: Web security for client sites (distilled from MDN CSP & XSS, the OWASP XSS Prevention Cheat Sheet, and HubSpot/Webflow platform docs).