KB Reports Web Security

Web Security for Agency Client Sites

Research brief · security category · 2026-05-27 · all sources accessed 2026-05-27

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].

TypeWhere injectedMechanism
Reflected (client-side)URL parameter reflected into the pageUnsanitized input assigned to an unsafe API like innerHTML[2]
Stored / persistentServer-side, during templatingUntrusted input stored, then served unsanitized to every visitor — most severe[2]
DOM-basedClient-side, in the browserDangerous 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].

ContextRule
HTML bodyConvert & < > to entities[3]
HTML attributeQuote the value; encode all chars in &#xHH; form[3]
JavaScriptOnly place data in quoted values; encode as \uXXXX[3]
CSSHex-encode (\XX) and only in property values[3]
URLPercent-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';

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

AutomaticConfigurable (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

  1. Prevent spam — CAPTCHA + honeypots[6]
  2. DDoS — firewalls, load balancers, WAF[6]
  3. Brute force — strong passwords, login-attempt limits, CAPTCHA, 2FA[6]
  4. XSS — install content security policies to filter hazardous scripts[6]
  5. SQL injection — parameterized queries, DB audits[6]
  6. SSL certificate — HTTPS everywhere[6]
  7. Backups — hosting with automatic backups[6]
  8. ISO 27018 compliance for personal-data hosting[6]
  9. Reliable payment gateways — PCI-DSS-compliant (Stripe, PayPal)[6]
  10. 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:

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

Sources

  1. [1] MDN — Content Security Policy (CSP): developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
  2. [2] MDN — Cross-Site Scripting (XSS): developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS
  3. [3] OWASP — XSS Prevention Cheat Sheet: cheatsheetseries.owasp.org/.../Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  4. [4] OWASP Foundation: owasp.org
  5. [5] HubSpot — SSL and domain security: knowledge.hubspot.com/domains-and-urls/ssl-and-domain-security-in-hubspot
  6. [6] Webflow — Website security checklist: webflow.com/blog/website-security-checklist