Not every web attack injects code. Some simply trick the browser, or the user, into sending requests they never intended. Cross-site request forgery (CSRF) and clickjacking are the classic examples, and both are defeated by headers and patterns you can add in an afternoon. This lesson covers how each works and the modern defense checklist.
Browsers historically attached cookies to every request to a domain, no matter which site initiated the request. CSRF abuses this: a malicious page causes the victim's browser to submit a request, for example a hidden auto-submitting form, to your site, and the browser helpfully includes the victim's session cookie. If your endpoint only checks the cookie, the forged request looks completely legitimate. Classic targets: changing an email address, transferring funds, deleting data. The attacker never sees the response; they only need the state change to happen.
The SameSite cookie attribute controls whether cookies accompany cross-site requests:
SameSite=Lax (the modern browser default): cookies are sent on top-level navigations but not on cross-site POSTs, killing the classic attack.SameSite=Strict: never sent cross-site, strongest but can log users out of legitimate deep links.// Express session cookie hardened against CSRF and theft
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
httpOnly: true, // no script access
secure: true, // HTTPS only
sameSite: 'lax', // not sent on cross-site POSTs
},
}));Do not rely on browser defaults; set the attribute explicitly.
The synchronizer token pattern embeds a random per-session token in every form and requires it back on submission. A cross-site attacker cannot read your pages (same-origin policy), so they cannot obtain the token, and their forged request fails validation. Most frameworks ship this: Django and Rails enable it by default; Express has middleware; check the box rather than reinventing it. For APIs consumed by JavaScript, a common equivalent is the double-submit cookie or a custom header requirement, since cross-site forms cannot set custom headers.
Origin or Sec-Fetch-Site request headers server-side and reject unexpected cross-site state changes.GET /delete?id=5 link is a CSRF gift.Clickjacking loads your site in an invisible iframe layered over a decoy page. The victim believes they are clicking 'Play video' but is actually clicking your 'Confirm transfer' button positioned exactly beneath the cursor. No code is injected; the user genuinely clicked your real button.
Two headers control framing:
# Modern, flexible (part of CSP)
Content-Security-Policy: frame-ancestors 'none'
# Legacy but still widely respected
X-Frame-Options: DENY
frame-ancestors 'none' forbids all framing; use 'self' if your own site legitimately embeds its pages. Send both headers for maximum browser coverage. If you offer an embeddable widget, allowlist specific partner origins instead of leaving framing open, and keep sensitive account pages framed by nobody.
A pragmatic checklist for every session-based application:
HttpOnly; Secure; SameSite=Lax at minimum.frame-ancestors 'none' plus X-Frame-Options: DENY unless you have a documented embedding need.These defenses are cheap, mostly declarative, and eliminate two entire attack classes.
Next lesson: The OWASP Top 10 Explained, the industry's map of the most critical web application risks.