XSS and CSRF Prevention, Rendering and Core Web Vitals
Prevent XSS with escaping, sanitising and CSP, and CSRF with SameSite cookies. Plus how the browser renders, defer vs async and the Core Web Vitals.
- Course: JavaScript study plan
- Module: The DOM and the browser
- Kind: Lesson
- Reading time: 13 min
- Runtime: Node 16
What is XSS and how do you prevent it?
Cross-site scripting (XSS) happens when attacker-controlled text is rendered as markup, so the attacker's script runs with your users' cookies, storage and API access. Prevent it in layers: render data with textContent or framework bindings, escape it when markup is unavoidable, sanitise user HTML with an allow-list library such as DOMPurify, send a Content Security Policy without 'unsafe-inline', and keep session cookies HttpOnly.
Lesson
A page can be correct and still fail its users in two ways: it can be slow — janky scrolling, a spinner over a white screen, an input that lags behind the keyboard — or it can be unsafe — a comment field that runs someone else's script in your users' sessions. Both have well-understood causes and defences. This lesson covers how the browser turns DOM and CSS into pixels and what makes that expensive; the loading path from HTML to interactive; the measurement tools and the vitals; then the front-end threat model: XSS and its defences (escaping, textContent, sanitisers, CSP), CSRF and SameSite, clickjacking, open redirects, third-party scripts and dependency risk.
From DOM to pixels
The browser builds the DOM and CSSOM, combines them into a render tree, computes layout (geometry: where everything is and how big), paints (draws pixels into layers), then composites the layers onto the screen. JavaScript can dirty any stage: changing text or a class → style recalculation → layout → paint → composite; changing only transform/opacity on a promoted layer → composite only (cheap — this is why animations use those two properties). Layout is proportional to the affected subtree; a change to font-size on body relays out everything. requestAnimationFrame batches your visual writes to just before the next frame; the frame budget at 60 Hz is 16.7 ms for everything — your JavaScript, style, layout, paint. A 30 ms handler drops frames.
Layout thrashing (module lesson 1): interleaved reads (offsetHeight, getBoundingClientRect) and writes force synchronous layout per iteration. Read first, then write; or use IntersectionObserver/ResizeObserver instead of measuring in scroll handlers.
The loading path
HTML streams in and parses; a <script> without defer/async blocks parsing until it downloads and runs (put scripts at the end or use defer, which runs in order after parsing; async runs as soon as downloaded, unordered — for independent scripts like analytics); CSS in <head> blocks rendering until loaded (critical CSS inline, the rest deferred); fonts can block text (font-display: swap); images without dimensions cause layout shifts (set width/height or aspect-ratio). Code splitting (dynamic import() per route), lazy-loading images (loading="lazy"), preloading the critical request (<link rel="preload">), compression and caching headers, and shipping less JavaScript are the levers — the last one dwarfs the rest.
Measuring
Core Web Vitals name what users feel: LCP (Largest Contentful Paint — when the main content shows, target < 2.5 s), INP (Interaction to Next Paint — input responsiveness, < 200 ms), CLS (Cumulative Layout Shift — things jumping, < 0.1). DevTools' Performance panel shows the frame timeline (long tasks > 50 ms block input); Lighthouse audits the loading path; PerformanceObserver reports vitals from real users (RUM). Measure before optimising; the profile usually points at one or two culprits — a large bundle, a synchronous layout loop, an unbatched render.
Rendering strategies
Manual DOM updates get slow to write correctly long before they get slow to run; a virtual DOM (React) or fine-grained reactivity (Solid, Vue, Svelte) exists to keep UI = f(state) while batching writes and updating only what changed. A virtual DOM diff compares two trees and emits patches (props changed, children inserted/removed/moved — keys make moves cheap). Server rendering (SSR) and static generation send HTML first so LCP does not wait for JavaScript; hydration then attaches behaviour. The trade-offs are the frameworks' whole subject; the principle is constant: batch, diff, and touch the real DOM as little as possible.
XSS — the front-end vulnerability
Cross-site scripting: attacker-controlled text is rendered as markup, so their <script> or onerror= runs with your users' cookies, storage and API access. Sources: innerHTML/outerHTML/insertAdjacentHTML/document.write with data, javascript: and data: URLs in href/src, eval/new Function on data, unsanitised markdown, server templates without auto-escaping. Defences, in layers:
- Render text as text:
textContent,createElement+ properties, framework bindings (React escapes{value};dangerouslySetInnerHTMLis the escape hatch with the warning in its name). - Escape at the point of insertion when markup is unavoidable:
& < > " '→ entities — context-aware (attribute, URL and script contexts differ). - Sanitise untrusted HTML that must stay HTML (user-authored rich text) with an allow-list library (DOMPurify) — never a hand-written regex.
- Content Security Policy header:
script-src 'self'(no inline scripts, noeval, only your origin) turns most XSS into a blocked-script console message; nonces/hashes for necessary inline scripts;object-src 'none'; report violations. - HttpOnly session cookies so a successful XSS cannot exfiltrate the session; tokens in memory rather than storage.
CSRF, clickjacking, redirects
CSRF: a malicious page makes the user's browser send a request to your site, with the user's cookies attached automatically. Defences: SameSite=Lax/Strict cookies (blocks the cross-site POST), anti-CSRF tokens in forms/headers for legacy cases, checking Origin/Sec-Fetch-Site headers, and never performing state changes on GET. Clickjacking: your page framed invisibly over a decoy so clicks land on your buttons — Content-Security-Policy: frame-ancestors 'none' (or X-Frame-Options). Open redirects: ?next=https://evil after login — allow only relative paths or an allow-list. target="_blank" links get rel="noopener" by default now; add noreferrer when the destination should not learn the referrer.
Third parties and dependencies
Every <script src="https://cdn…"> runs with full page privileges; a compromised CDN or tag manager is a total compromise. Pin with Subresource Integrity (integrity="sha384-…"), self-host where possible, limit with CSP, and audit the npm tree (module 9) — a front-end bundle is as much other people's code as a server. Store nothing in the page a script should not read.
Common mistakes
- Animating
top/left/widthinstead oftransform; measuring layout inside scroll handlers. - Blocking
<script>tags in<head>; images without dimensions (CLS); one giant bundle. innerHTMLwith any user-influenced string; sanitising with a regex;hreffrom data without a scheme allow-list.- No CSP, or a CSP with
'unsafe-inline'that defends nothing; session cookies withoutHttpOnly/SameSite. - State-changing GET endpoints;
next=redirects without validation; third-party scripts without SRI.
Interview angle
- "What happens between a DOM change and pixels?" Style → layout → paint → composite;
transform/opacityskip layout and paint. - "
deferversusasync?" Both download in parallel;deferruns in order after parsing,asyncas soon as it arrives, unordered. - "What is XSS and how do you prevent it?" Untrusted text rendered as markup; render as text, escape, sanitise with an allow-list, CSP, HttpOnly cookies.
- "What is CSRF and the modern defence?" Cross-site requests riding the user's cookies;
SameSitecookies plus tokens where needed; no state changes on GET. - "Name the Core Web Vitals." LCP (loading), INP (responsiveness), CLS (visual stability).
Key takeaways
- Style → layout → paint → composite; batch writes, avoid read/write interleaving, animate
transform/opacity, stay inside the 16.7 ms frame. - Loading:
deferscripts, critical CSS, image dimensions, code splitting, less JavaScript; measure LCP/INP/CLS. - XSS: text as text, escape, sanitise with an allow-list, CSP without
'unsafe-inline', HttpOnly cookies. - CSRF:
SameSitecookies and tokens, no GET mutations;frame-ancestorsagainst clickjacking; validate redirects. - Third-party scripts are full-privilege code: SRI, self-host, CSP, audit.
Common questions
What is CSRF and how do you prevent it?
Cross-site request forgery (CSRF) is a malicious page making the user's browser send a request to your site with the user's cookies attached automatically. The modern defence is SameSite=Lax or Strict session cookies, which block the cross-site POST, plus anti-CSRF tokens where cookies must be SameSite=None, checks of the Origin header, and no state changes on GET.
What is the difference between defer and async on a script tag?
Both download the script in parallel with HTML parsing. defer runs scripts in document order after parsing finishes; async runs each one as soon as it has downloaded, in no guaranteed order, which suits independent scripts such as analytics. A script tag with neither attribute blocks parsing until it downloads and runs.
What are the Core Web Vitals?
Three measures of what users feel: Largest Contentful Paint (LCP), when the main content shows, with a target under 2.5 seconds; Interaction to Next Paint (INP), input responsiveness, under 200 milliseconds; and Cumulative Layout Shift (CLS), visual stability, under 0.1. Measure them in the lab with Lighthouse and from real users with PerformanceObserver.
Why animate transform and opacity instead of top and left?
Changing top, left or width forces the browser through layout and paint on every frame, while transform and opacity on a promoted layer only need compositing, the cheapest stage of the pipeline. At 60 Hz each frame has about 16.7 milliseconds for all JavaScript, style, layout and paint, so skipping stages keeps animations smooth.
Exercises
Count the reflows
The starter models the layout engine: write() marks layout dirty, read() forces a reflow when dirty (counting it). Starting dirty each time, implement three strategies over the input heights and end each with the frame's own read(): naive — for each item write then read (measure after mutating); batched — read every item first, then write every item; requestAnimationFrame — read each item immediately but queue the writes in a fake requestAnimationFrame, then run the frame. Print naive: reflows=<n> for <n> items, batched: reflows=<n>, requestAnimationFrame: reflows=<n>, sameResult=<all three produced the same doubled heights> out=<JSON>.
Example: 10 20 30 40 50 →
naive: reflows=5 for 5 items
batched: reflows=2
requestAnimationFrame: reflows=2
sameResult=true out=[20,40,60,80,100]
Interleaving a read after every write costs one layout per item; separating the phases costs one layout per phase.
Sanitise untrusted HTML and check a CSP
Line 1 is untrusted HTML, line 2 a Content-Security-Policy header, the rest are script sources to test. Write sanitize(markup): split into tags and text; keep only b i p a br (an <a> keeps only an http(s) href, re-emitted as href="…" rel="noopener"); drop every other tag — and the whole content of <script>/<style> — recording what was dropped (onclick-style attributes as onclick, a rejected link as a[href=javascript:]); escape all text with an escaper that leaves existing entities alone. Print sanitized=<html> and dropped=<comma list or ->. Then cspAllows(policy, source) for the script-src directive: inline needs 'unsafe-inline', eval needs 'unsafe-eval', self needs 'self', a URL matches a host source exactly or a *. wildcard; no directive → nothing allowed. Print <source>: allowed|blocked.
Example (excerpt):
sanitized=<p>Hi <b>Ada</b> & <a href="https://x.io" rel="noopener">link</a><a>bad</a></p>
dropped=onclick,script,img,a[href=javascript:]
inline: blocked
https://a.trusted.net/x.js: allowed
https://evil.example/x.js: blockedIn this module: The DOM and the browser
- The DOM — the tree behind the page, and how to change it safely
- Events — propagation, delegation and the event object
- fetch, URLs, CORS and talking to servers
- Storage, cookies, history and the browser APIs worth knowing by name
- Rendering performance and front-end security — the two things a page must not get wrong (this lesson)
- Checkpoint — The DOM and the browser
← Storage, cookies, history and the browser APIs worth knowing by name · Checkpoint — The DOM and the browser →