The Mental Model For Web Security
Web security is not a checklist you run through once. It is a way of looking at every feature you ship and asking what happens if the person using it is not who they say they are.
I used to think "sanitize your inputs" was most of what web security meant. Get that right, add HTTPS, maybe a rate limiter, and you were done.
Then I actually sat down and broke a few intentionally vulnerable apps, and realized almost none of my worst assumptions were true. Not because the apps were badly written. Because the web itself is built on decades of decisions that were never meant to hold up the way they do today.
The Web Was Never Supposed to Be This
The web started as a text document viewer. Someone had the idea that a word, if you clicked it, could take you to another document.
That idea became the largest distributed application platform ever built. A browser today compiles and runs your code on a stranger's machine, with no install step, no download, nothing but a URL. That is genuinely one of the best things about how we build software.
It is also the scariest part, for the exact same reason.
Decades of Ad Hoc Decisions
Because the web grew this way, a lot of what holds it together was never designed so much as shipped and left alone. Cookies are a good example, and the next post in this series goes deep on them.
For now, the point is this: a meaningful chunk of what makes web security hard is not bad code. It is inherited behavior from choices made in the 1990s that nobody can undo without breaking the entire internet.
Browsers cannot just fix these things overnight, even the ones everyone agrees are flawed. Too much of the web already depends on the flawed behavior staying exactly as it is.
That is why understanding web security means understanding some history alongside the technology. You cannot reason about why a rule exists without knowing what it was reacting to.
Why a Checklist Is the Wrong Mental Model
"Sanitize your inputs" is true and also nearly useless on its own. It tells you what to do without teaching you how to recognize the fifty situations where you forgot to.
Most serious security bugs are not the result of an engineer not caring. They are one of two things:
- Nobody on the team knew a particular attack was even possible
- Everyone knew, but a code review comment said "we have to ship it this way for now" and nobody circled back
Both of those are failures of mental model, not failures of effort. A checklist cannot fix either one, because a checklist only catches the specific items already on it.
The better approach is learning to notice when something feels off, the same instinct a senior engineer has when a pull request smells wrong before they can articulate why. You build that instinct by understanding how these attacks actually work, from the attacker's side, not by memorizing a list of dos and don'ts.
Nobody Really Knows Who You Are
Here is the part that reframes everything else: on the internet, identity is a claim, not a fact.
A server does not know you are you. It knows a request arrived carrying a cookie or a token that matches what it expects, and it trusts that match completely. If someone else can produce that same match, the server has no way to tell the difference.
That single fact is the foundation for most of what this series covers. Sessions, cookies, CSRF tokens, all of it exists because "prove you are who you say you are, on every single request, without asking the user to type a password every time" is a genuinely hard problem.
And here is the uncomfortable part for anyone who thinks of themselves as "just a frontend engineer": the frontend is the front line of this problem. Every input field, every cookie your app sets, every fetch call is a door someone else can try to open.
Your Framework Does Not Save You
It is tempting to believe that using React, or Next.js, or whatever framework happens to be popular this year, means these problems are handled for you.
They are not. A framework can escape your JSX by default, or ship a CSRF middleware, and still leave you completely open the moment your own business logic trusts something it should not.
Frameworks close entire categories of bugs. They cannot close the specific hole you create when you store a user's identity somewhere an attacker can read or edit it. That is a decision you make, not one your tooling makes for you.
Layers, Not a Single Fix
The other habit worth building early is thinking in layers instead of silver bullets. By the time this series covers signing cookies and real sessions and same-origin policy, you will see this pattern repeat: no single attribute or header is ever the whole answer.
ExpandThree stacked layers: cookie attributes, sanitization plus CSP, and origin checks plus CSRF tokens.
Almost nothing in this series is "do this one thing and you are safe." It is closer to "do these three unrelated things, and a mistake in any one of them still leaves the other two standing." A signed, HttpOnly cookie plus a content security policy plus a properly scoped CORS setup is a very different situation than any one of those alone.
There are real trade-offs here too. The strongest possible session security might mean logins that take longer, or infrastructure you cannot justify for a small app. Part of thinking clearly about security is knowing which trade-offs are worth making for the app you actually have, not the app a conference talk assumes you have.
How the Rest of This Series Works
The posts ahead are built around breaking small, intentionally vulnerable apps rather than reciting policy. Each one runs on Express and Node, backed by SQLite, with a handful of demo users already loaded in.
The approach throughout is consistent: look at a feature, figure out how to exploit it, then fix it properly and understand why the fix works. That order matters. Seeing the exploit first is what makes the fix feel inevitable instead of arbitrary.
The first stop is the piece of technology behind almost every login on the web, and one of the oldest, weirdest hacks the internet ever agreed to: the cookie.
The Essentials
- The web's biggest strength, running code instantly with no install, is also its biggest attack surface. Every page you load executes on your machine with no gatekeeper beyond the browser itself.
- Most of the web's security quirks are inherited history, not bad design. Decades-old decisions cannot be undone without breaking the internet, so they get worked around instead.
- A checklist catches known items. A mental model catches the ones nobody wrote down yet. Most real incidents come from a gap in understanding, not a gap in effort.
- Identity on the web is a claim the server chooses to trust, not a verified fact. Anyone who can reproduce that claim can become that user.
- Your framework closes categories of bugs. It cannot close the one you create in your own business logic.
- Security holds up in layers. A weakness in one layer should still leave the others standing.
Up next: the single most common way identity gets faked on the web, and the reason it works at all despite HTTP having no concept of "logged in" to begin with.
Further Reading and Watching
- What is Defense in Depth?: a short breakdown of layered security and why no single control is meant to carry the whole defense.
- OWASP Top 10: the standard reference for the most common categories of web application risk, worth skimming before this series goes deeper into specific ones.
Keep reading