Today’s Lesson
Security for Legal SaaS — Episode 12: Browser Security Headers
The Browser as Battleground
Your legal SaaS application runs in your users’ browsers. Those browsers will happily execute JavaScript from any origin, embed your pages in iframes, and send credentials to any domain — unless you explicitly tell them not to. Browser security headers are instructions from your server to the browser, declaring what is and isn’t permitted. Get them wrong, and you’ve handed attackers a proxy inside your user’s authenticated session.
Key stat: The 2023 Verizon DBIR found that web application attacks constituted 26% of all breaches, with credential theft and session hijacking as primary objectives. Properly configured security headers eliminate entire classes of these attacks.
For legal tech — where a single authenticated session may access privileged case files, billing data, and client communications — browser-level protections are not optional polish. They are the last line of defence when everything else fails.
CORS: Who Gets to Read Your Responses
Cross-Origin Resource Sharing (CORS) controls which external origins can make requests to your API and read the responses. The Same-Origin Policy blocks cross-origin reads by default — CORS relaxes it selectively.
The Misconfiguration That Opens Everything
| Configuration | Risk Level | Effect |
|---|---|---|
Access-Control-Allow-Origin: * |
Critical (with credentials) | Any website can read your API responses |
| Reflecting the Origin header back | Critical | Attacker’s domain gets access automatically |
Access-Control-Allow-Credentials: true + wildcard |
Critical | Explicitly forbidden by spec but some servers try |
| Specific trusted origins only | Correct | Only your own domains can read responses |
Real-world example: PortSwigger research demonstrated that CORS misconfigurations in production applications allowed attackers to steal user data cross-origin by simply reflecting the attacker's Origin header in the Access-Control-Allow-Origin response.
For legal SaaS: If your document API reflects any Origin header and includes Access-Control-Allow-Credentials: true, an attacker’s website can silently read your user’s privileged case documents while they have an active session.
Correct Implementation
Maintain a strict allowlist of permitted origins. Validate the incoming Origin header against this list. Only echo back origins that match exactly. Never use regex patterns that can be bypassed (e.g., *.lawfirm.com matching evil-lawfirm.com).
CSRF: Forged Actions in Your User’s Name
Cross-Site Request Forgery tricks a user’s browser into making state-changing requests to your application using their existing session cookies. The browser automatically attaches cookies to same-site requests — the attacker doesn’t need to steal credentials.
Attack Scenario in Legal Tech
A lawyer visits a malicious page (perhaps linked in an email from opposing counsel). The page contains a hidden form that submits to your application’s API:
- POST /api/matters/{id}/share — shares a privileged case with an external email
- POST /api/documents/{id}/access — grants document access to a new user
- POST /api/billing/transfer — initiates a trust account transfer
Defence Layers
| Defence | How It Works |
|---|---|
| CSRF tokens | Server generates a random token per session; form submissions must include it. Attacker’s page cannot read it (Same-Origin Policy). |
| SameSite cookies | SameSite=Lax (default in modern browsers) blocks cookies on cross-site POST requests. Strict blocks them on all cross-site requests. |
| Origin/Referer validation | Verify the Origin or Referer header on state-changing requests matches your domain. |
| Custom request headers | Require a custom header (e.g., X-Requested-With) that simple cross-origin forms cannot set. |
The SameSite cookie attribute has dramatically improved the baseline — Chrome, Firefox, and Edge all default to Lax. But explicit CSRF tokens remain necessary for older browser support and defence-in-depth.
How CSRF Actually Works
The attack exploits two facts about how browsers handle cookies. First, when you log into a site, your browser stores a session cookie and automatically attaches it to every subsequent request to that domain — you don’t re-authenticate on each click. Second, the target server blindly trusts any request bearing a valid cookie, assuming you intentionally initiated it.
Here’s the sequence: you log into bank.com and your session cookie is active. In a separate tab, you visit a malicious page. That page contains a hidden HTML form pointing at bank.com/transfer with pre-filled values (recipient: attacker, amount: $1,000). A tiny script auto-submits the form the instant the page loads. Your browser fires the POST request to bank.com — and because the destination is bank.com, it automatically glues your login cookie onto the request. The bank receives an authentic-looking request with your valid session, processes the transfer, and you never saw a thing.
The attacker’s payload is trivially simple — a hidden form with a JavaScript one-liner to submit it:
<form id="csrfForm" action="https://bank.com/transfer" method="POST">
<input type="hidden" name="toAccount" value="hacker_123" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>document.getElementById('csrfForm').submit();</script>
A common misconception is that browser tab isolation should prevent this. The browser’s Same-Origin Policy does block a malicious site from reading data from another tab. But it does not stop it from sending data. CSRF only needs to send a blind command — it doesn’t care about the response. That’s the loophole.
The two primary defences close this gap. Anti-CSRF tokens embed a server-generated random value in legitimate forms; the attacker cannot read it cross-origin, so forged submissions fail validation. SameSite cookie attributes (Lax or Strict) instruct the browser to strip cookies from cross-site requests entirely, removing the automatic attachment that makes the attack possible.
Content Security Policy (CSP)
CSP tells the browser exactly which sources of content are permitted — scripts, styles, images, fonts, frames, and more. A strict CSP is the most effective defence against Cross-Site Scripting (XSS).
CSP for Legal SaaS
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{random}';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://cdn.yourdomain.com;
frame-ancestors 'none';
form-action 'self';
base-uri 'self';
| Directive | Purpose |
|---|---|
script-src 'self' 'nonce-{random}' |
Only execute scripts from your domain or with a per-request nonce |
frame-ancestors 'none' |
Prevent your app from being embedded in iframes (replaces X-Frame-Options) |
form-action 'self' |
Forms can only submit to your own origin |
base-uri 'self' |
Prevents <base> tag injection that redirects relative URLs |
Deployment tip: Start with Content-Security-Policy-Report-Only to collect violation reports without breaking functionality. Report URI or your own endpoint receives JSON reports of what would have been blocked. Tighten over weeks, then enforce.
X-Frame-Options and Clickjacking
X-Frame-Options prevents your application from being embedded in an iframe on an attacker’s page. Without it, an attacker overlays transparent iframes of your app over a decoy page — the user thinks they’re clicking a harmless button but actually clicking “Share Document” or “Approve Transfer” in your application.
For legal SaaS: X-Frame-Options: DENY (or use the equivalent frame-ancestors 'none' in CSP). There is almost never a legitimate reason to allow external framing of a legal application.
HSTS: Forcing HTTPS
HTTP Strict Transport Security (HSTS) tells browsers to only connect to your domain over HTTPS — never HTTP. This prevents SSL stripping attacks where a man-in-the-middle downgrades the connection.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Submitting to the HSTS preload list hardcodes your domain into browser source code — HTTPS is enforced even on the first visit, before any header is received.
Headers as Defence-in-Depth
No single header stops all attacks. The security model is layered:
| Attack | Primary Defence | Header Backup |
|---|---|---|
| XSS | Input sanitisation + output encoding | CSP blocks inline script execution |
| Clickjacking | Frame-busting JS | X-Frame-Options / CSP frame-ancestors |
| MITM downgrade | TLS configuration | HSTS forces HTTPS |
| Session hijacking | Secure cookie flags | SameSite + CSP prevents exfiltration vectors |
| Data theft via embedding | Authentication | CORS blocks cross-origin reads |
Automated Scanning
Don’t rely on manual header checks. Mozilla Observatory grades your site’s security headers and provides specific remediation guidance. SecurityHeaders.com by Scott Helme offers instant analysis with explanations for each missing or misconfigured header.
Run these scans in CI/CD — a deployment that removes a security header should fail the pipeline, not reach production.
Conclusion
Browser security headers are cheap to implement and expensive to forget. A single misconfigured CORS policy can expose every privileged document your users access. A missing CSP allows injected scripts to exfiltrate session tokens. For legal SaaS, where the browser session is the gateway to attorney-client privilege, these headers are not optimisation — they are infrastructure.
Next episode: TLS and HTTPS from Scratch — certificates, cipher suites, and why “just add HTTPS” involves more decisions than you’d expect.