KB — Reports — Web Security
Web Security for Agency Client Sites
Executive summary
Cross-site scripting (XSS) is executing attacker-supplied input as if it were the site's own code[2]. The durable defense is layered: framework auto-encoding by output context, sanitize HTML you must render (DOMPurify), and a strict nonce/hash Content-Security-Policy as a backup that blocks injected scripts even if they reach the page[1][3]. For the platforms Stormfors ships on: HubSpot auto-provisions SSL and lets you set HSTS + a CSP header in domain settings[5]; Webflow's checklist covers SSL, CSP, 2FA, spam/WAF, and patching[6]. No single control solves XSS — defense in depth is mandatory[3].
1. What XSS is and how it works
An XSS attack tricks a target site into executing malicious code within its own origin, subverting the same-origin policy[2]. Every XSS depends on two things: the site accepts attacker-craftable input, and it includes that input in a page without ensuring it can't run as JavaScript[2]. Successful code can read/modify all page content and local storage, and make authenticated HTTP requests as the user — enabling impersonation and data theft[2].
| Type | Where injected | Mechanism |
|---|---|---|
| Reflected (client-side) | URL parameter reflected into the page | Unsanitized input assigned to an unsafe API like innerHTML[2] |
| Stored / persistent | Server-side, during templating | Untrusted input stored, then served unsanitized to every visitor — most severe[2] |
| DOM-based | Client-side, in the browser | Dangerous Web APIs (innerHTML, document.write(), eval()) called with unsanitized input[2] |
2. XSS prevention — the layered framework
OWASP is explicit: "no single technique will solve XSS" — defenses must be layered across framework protections, output encoding, and HTML sanitization[3].
Output encoding by context
Encoding must match where the data lands; the correct escaping differs per context[2][3].
| Context | Rule |
|---|---|
| HTML body | Convert & < > to entities[3] |
| HTML attribute | Quote the value; encode all chars in &#xHH; form[3] |
| JavaScript | Only place data in quoted values; encode as \uXXXX[3] |
| CSS | Hex-encode (\XX) and only in property values[3] |
| URL | Percent-encode parameters only[3] |
Never interpolate untrusted data directly into <script> tags, HTML comments, CSS selectors, event handlers (onclick), or eval()/setTimeout() — these contexts are effectively unsafe[2][3]. Prefer safe sinks: .textContent, .setAttribute() with a hardcoded name, createTextNode()[3].
Frameworks do most of this for you
Modern frameworks auto-encode by default — Django and React/JSX escape interpolated values automatically[2]. The risk is the escape hatches: React's dangerouslySetInnerHTML, Angular's bypassSecurityTrustAs*, Lit's unsafeHTML[3].
Sanitize HTML you must render
When users author HTML (WYSIWYG), encoding breaks functionality — sanitize instead with DOMPurify, which OWASP endorses: const clean = DOMPurify.sanitize(dirty);[2][3]. Trusted Types (Chromium) can force every DOM sink through a vetted policy[2][3].
3. Content Security Policy as the backup layer
CSP is an HTTP header instructing the browser to restrict what site code may do; its primary use is defending against XSS by controlling which scripts load and run[1]. By default a CSP blocks inline <script> tags, inline event handlers, javascript: URLs, and eval()[1]. It is not a substitute for sanitization — sanitize input and set a CSP for defense in depth[1].
Prefer strict (nonce/hash) over allowlist
Allowlist CSPs are easily bypassed and become unwieldy — integrating Google Analytics alone can require allowlisting ~187 domains[1]. OWASP also treats CSP as a supplementary layer, not the primary defense[3]. A strict policy:
Content-Security-Policy:
script-src 'nonce-{RANDOM_PER_RESPONSE}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
- Nonce — a cryptographically random value, unique per HTTP response, echoed in the header and each
<script nonce=...>. Best for server-rendered dynamic content[1]. - Hash — a
sha256-…of the script body; needs no server templating, so it works for static HTML, but must be recomputed when the script changes[1]. strict-dynamic— lets a trusted (nonce/hash) script load further scripts without their own nonce, solving third-party loaders[1].
Deploy safely, then enforce
Test with Content-Security-Policy-Report-Only — violations are reported but not blocked[1]. Wire violation reports via the Reporting API (Reporting-Endpoints + report-to)[1]. CSP also covers clickjacking (frame-ancestors 'none') and HTTPS upgrades (upgrade-insecure-requests)[1]. Caveat: a blanket enterprise-wide CSP breaks legacy apps and browser support varies[3].
4. Platform specifics — what Stormfors actually ships on
HubSpot
| Automatic | Configurable (Super Admin / Domain perm) |
|---|---|
| SAN SSL via Google Trust Services on domain connect — active in minutes, up to 4h; auto-renews 30 days before expiry while the CNAME points to HubSpot[5] | HTTPS enforcement; minimum TLS version; HSTS (max-age, preload, include-subdomains)[5] |
| TLS 1.2+ accepted by default[5] | Security headers: X-Frame-Options, X-Content-Type-Options, Content-Security-Policy, Referrer-Policy, Permissions-Policy[5] |
Custom SSL add-on allows uploading your own certificate, but a pre-existing certificate can't be reused (it compromises certificate security)[5]. Practical takeaway: on HubSpot you can set a CSP and HSTS directly in domain security settings without external infrastructure.
Webflow — 10-point checklist
- Prevent spam — CAPTCHA + honeypots[6]
- DDoS — firewalls, load balancers, WAF[6]
- Brute force — strong passwords, login-attempt limits, CAPTCHA, 2FA[6]
- XSS — install content security policies to filter hazardous scripts[6]
- SQL injection — parameterized queries, DB audits[6]
- SSL certificate — HTTPS everywhere[6]
- Backups — hosting with automatic backups[6]
- ISO 27018 compliance for personal-data hosting[6]
- Reliable payment gateways — PCI-DSS-compliant (Stripe, PayPal)[6]
- Regular updates — keep CMS/plugins/themes current[6]
5. OWASP — the reference body
OWASP is the Open Source Foundation for Application Security, a volunteer-driven non-profit aiming to make software security visible[4]. The Top 10 is the reference standard for the most critical web-app security risks[4]. Other flagship resources: ASVS (verification standard), the Cheat Sheet Series (used in §2 above), and Juice Shop (intentionally vulnerable training app)[4].
Recommendation
For Stormfors client sites, adopt a standard security baseline per engagement:
- ✓ Always-on: HTTPS/SSL, HSTS, 2FA on the CMS, automatic backups, and current platform versions. On HubSpot these are toggles in domain settings[5]; Webflow handles SSL and provides the checklist[6].
- ✓ For any custom code / embeds: rely on the framework's auto-encoding, never interpolate into
<script>or event-handler contexts, and sanitize author-supplied HTML with DOMPurify[2][3]. - ✓ Add a strict nonce/hash CSP (with
object-src 'none'; base-uri 'none'), deployed first inReport-Onlymode, then enforced[1]. On HubSpot, set it via the CSP domain header[5].
Cost/effort: mostly configuration, not engineering — the toggles are free on both platforms. The real effort is the CSP: hash-based CSP suits static Webflow exports but needs recomputation on script changes[1]; nonce-based needs server-side rendering, which static hosting lacks.
Residual risk / human judgment: Webflow-exported pages are static, so nonce CSP isn't available there — this needs a decision (hash CSP vs. accept allowlist limits). DOM-based XSS isn't caught by WAFs[3], so code review of any custom JS remains necessary.
Unknowns & assumptions
- unverified Exact CSP header configuration UI/limits inside HubSpot (max length, per-domain vs. global) were not detailed on the source page — only that the header is configurable[5]. Confirm in HubSpot before scoping.
- unverified Whether Webflow's hosting lets you set arbitrary response headers (CSP/HSTS) without a reverse proxy. The Webflow article recommends CSPs[6] but does not state how. Assumption: custom security headers on Webflow typically require a proxy (e.g. Cloudflare) in front — verify per project.
- unverified OWASP Top 10 current edition/year was not enumerated on the landing page fetched[4]; consult the dedicated Top 10 project page if a per-item mapping is needed.
- Assumption: the OWASP and Webflow blog pages reflect 2026-current guidance; CSP/XSS fundamentals (MDN, OWASP cheat sheet) are stable and not date-sensitive.
Sources
- [1] MDN — Content Security Policy (CSP): developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
- [2] MDN — Cross-Site Scripting (XSS): developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS
- [3] OWASP — XSS Prevention Cheat Sheet: cheatsheetseries.owasp.org/.../Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- [4] OWASP Foundation: owasp.org
- [5] HubSpot — SSL and domain security: knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot
- [6] Webflow — Website security checklist: webflow.com/blog/website-security-checklist