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

  1. 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, Angular bypassSecurityTrustAs*, Lit unsafeHTML).
  2. 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.
  3. Never interpolate untrusted data into <script> tags, HTML comments, CSS selectors, event handlers (onclick), or eval()/setTimeout(). Prefer safe sinks: .textContent, .setAttribute() with a hardcoded name, createTextNode().
  4. Sanitize HTML you must render. When users author HTML (WYSIWYG), encoding breaks it — use DOMPurify: const clean = DOMPurify.sanitize(dirty);
  5. Set a strict CSP as the backup layer:
    Content-Security-Policy:
      script-src 'nonce-{RANDOM_PER_RESPONSE}' 'strict-dynamic';
      object-src 'none';
      base-uri 'none';
    Use a per-response nonce (server-rendered) or a sha256-… hash (static HTML). Deploy as Content-Security-Policy-Report-Only first, 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).