09 - Security ​
1. SameSite Cookie Modes ​
What: Controls when cookies are sent with cross-site requests, defending against CSRF attacks.
SameSite=Strict
Cookie is ONLY sent for same-site requests.
Clicking a link from email to your site → cookie NOT sent → user must re-login.
Most secure but worst UX.
SameSite=Lax (DEFAULT in modern browsers)
Cookie sent for same-site requests + top-level navigations (clicking links).
NOT sent for cross-site POST, iframe, AJAX, image loads.
Good balance of security + UX.
SameSite=None; Secure
Cookie sent with ALL requests (including cross-site).
MUST have Secure flag (HTTPS only).
Required for: third-party cookies, cross-origin API calls with cookies,
embedded checkout SDKs in iframes.Example — checkout SDK in iframe:
Set-Cookie: session=abc123; SameSite=None; Secure; HttpOnly
// SameSite=None needed because the SDK runs in an iframe on a merchant's domain
// (different origin = cross-site)2. CSRF vs XSS Mitigation ​
CSRF (Cross-Site Request Forgery) ​
What: Attacker tricks an authenticated user into submitting a request to your API.
User is logged into mybank.com (has session cookie).
User visits evil.com which has:
<form action="https://mybank.com/api/transfer" method="POST">
<input name="to" value="attacker-account">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
// Browser sends the form WITH the session cookie → transfer happens!Mitigations:
- SameSite cookies (Lax/Strict) — cookie not sent from cross-site form
- CSRF tokens — unique token per session/form, server validates it
- Check Origin/Referer headers — reject requests from unexpected origins
- Double-submit cookie pattern — CSRF token in both cookie and request body
// CSRF token pattern
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken, // server validates this matches session
},
body: JSON.stringify({ to: 'account', amount: 100 }),
});XSS (Cross-Site Scripting) ​
What: Attacker injects malicious script into your page, which runs with the user's privileges.
Types:
- Stored XSS: Malicious script saved in DB, served to all users (
<script>steal(cookies)</script>in a comment field) - Reflected XSS: Malicious script in URL, reflected in response (
/search?q=<script>alert('xss')</script>) - DOM XSS: Script manipulates DOM directly via client-side JS (
element.innerHTML = userInput)
Mitigations:
- Escape output — React does this by default (JSX escapes strings)
- Never use
dangerouslySetInnerHTML— or sanitize with DOMPurify first - Content Security Policy (CSP) — blocks inline scripts
- HttpOnly cookies — JS can't access auth cookies
- Input validation — validate and sanitize on server
// React is safe by default
<p>{userInput}</p> // userInput = "<script>alert('xss')</script>"
// Renders as text, not executed
// DANGEROUS:
<div dangerouslySetInnerHTML={{ __html: userInput }} /> // XSS!
// Safe with sanitization:
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />3. Content Security Policy (CSP) ​
What: HTTP header that tells the browser which sources of content are allowed. Blocks inline scripts, unauthorized script sources, and other injection vectors.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://images.example.com;
connect-src 'self' https://api.example.com;
frame-src https://checkout.example.com;
font-src 'self' https://fonts.gstatic.com;
object-src 'none';
base-uri 'self';Key directives:
| Directive | Controls |
|---|---|
script-src | JavaScript sources |
style-src | CSS sources |
connect-src | fetch/XHR/WebSocket targets |
img-src | Image sources |
frame-src | iframe sources |
default-src | Fallback for unspecified directives |
Nonce-based CSP (for inline scripts):
<!-- Server generates unique nonce per request -->
Content-Security-Policy: script-src 'nonce-abc123';
<script nonce="abc123">
// This runs because nonce matches
</script>
<script>
// This is BLOCKED — no matching nonce
</script>Report-only mode (for testing):
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports
// Doesn't block anything, just logs violations — use during rollout4. Trusted Types ​
What: A browser API that prevents DOM XSS by requiring typed objects instead of raw strings for dangerous DOM sinks.
DOM sinks (dangerous operations):
element.innerHTML = stringdocument.write(string)eval(string)script.src = stringa.href = string
Without Trusted Types:
element.innerHTML = userInput; // potential XSS — browser allows itWith Trusted Types:
// Enable via CSP
// Content-Security-Policy: require-trusted-types-for 'script'
element.innerHTML = userInput; // BLOCKED — TypeError: not a TrustedHTML
// Must create a policy
const policy = trustedTypes.createPolicy('sanitize', {
createHTML: (input) => DOMPurify.sanitize(input),
});
element.innerHTML = policy.createHTML(userInput); // Allowed — sanitizedWhy it matters: Prevents ALL DOM XSS at the browser level, not just the ones you remember to sanitize.
5. DOM Clobbering ​
What: An attack where HTML elements with id or name attributes overwrite global JavaScript variables, because browsers make elements accessible via window.elementId and document.elementName.
<!-- Attacker injects this HTML (via stored XSS, user-generated content, etc.) -->
<img id="config">
<a id="config" name="config" href="https://evil.com/config.json">
<!-- Your JavaScript: -->
<script>
// You expect window.config to be your app config
// But it's now the DOM element!
console.log(window.config); // HTMLImageElement or HTMLCollection
console.log(config.href); // "https://evil.com/config.json"
// If your code does:
fetch(config.href); // fetches from attacker's server!
</script>Mitigations:
- Don't rely on global variables — use
const, modules, closures - Use
Object.freeze()on config objects - Sanitize user HTML (DOMPurify removes clobbering vectors)
- Use CSP to prevent unauthorized script execution
- Always check types:
if (typeof config === 'object' && !(config instanceof HTMLElement))
6. Prototype Pollution ​
What: An attacker modifies Object.prototype, affecting ALL objects in the application.
// Vulnerable merge function
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// Attacker sends this JSON:
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, malicious);
// Now EVERY object has isAdmin:
const user = {};
console.log(user.isAdmin); // true! Prototype is polluted.Real impact:
// In your code somewhere:
if (user.isAdmin) {
showAdminPanel(); // attacker gets admin access
}Mitigations:
- Check for
__proto__,constructor,prototypekeys:jsif (key === '__proto__' || key === 'constructor' || key === 'prototype') continue; - Use
Object.create(null)for dictionaries (no prototype chain) - Use
Mapinstead of plain objects for dynamic keys - Freeze prototype:
Object.freeze(Object.prototype)(aggressive) - Use safe merge libraries (lodash's
mergeis safe in recent versions) - Validate JSON input on the server before processing