Lesson 1 of 5
What Security Actually Means
"Amateurs hack systems, professionals hack people." — Bruce Schneier, Secrets and Lies (2000)
Most engineers meet security as a list. A checklist from a compliance form, a report from a scanner, a set of headers someone said to add. Lists are useful and they teach the wrong thing, because a list tells you what somebody found last time and nothing about how to look.
This volume is about how to look. Security is not a feature you add or a state you reach; it is a property of a system under an adversary, and the only durable skill is being able to read a design and see where it gives way. That skill is learnable, it is mostly not about cryptography, and it starts with three ideas that reframe everything after them.
The first is that security is economic, not binary. There is no secure system, only systems where attacking costs more than the attacker gains. Every control you add raises that cost; none of them reaches zero risk, and pretending otherwise is how teams spend a quarter on the wrong thing.
The second is the asymmetry: you must hold every path, an attacker needs one. In the small application modelled below that is a seven-to-one disadvantage before anyone writes a line of code, and it explains why "we reviewed it carefully" is not a strategy.
The third is that a vulnerability is not a risk. A flaw nobody can reach, in code nobody runs, scores zero — and treating every scanner finding as equally urgent is a reliable way to exhaust a team while the actual exposure sits untouched.
A scanner reports 340 findings. Your team can fix perhaps 30 this quarter. Half the findings are in code paths that cannot be reached from the internet, and one of them is a trivially reachable flaw in the payments flow. What question separates the 30 that matter from the 310 that do not — and why does severity alone fail to answer it?
Security is economic
Run to execute the tests…
The fourth test is the one worth acting on. Three of the five levers are not about prevention — reducing what an attacker gains and raising their odds of being caught are defences, and they are the ones most teams reach for last.
A bicycle lock is not rated "secure". It is rated in minutes — how long a given tool takes to defeat it. Nobody finds that unsatisfying, because everyone understands the model: you buy enough minutes that a thief moves to the bike beside yours, and you buy more minutes for a more expensive bike in a worse neighbourhood. Software security is exactly this and we describe it with a word that implies a binary, which is most of why the conversation goes wrong.
The asymmetry
Run to execute the tests…
The fifth test is the strategic point of the whole volume. Fixing a bug removes one path; removing a class changes the count. Parameterised queries do not fix a SQL injection — they end SQL injection.
A vulnerability is not a risk
Run to execute the tests…
The question I ask about any finding, before severity, is: who can reach this, and what do they get? Two clauses, and they do more prioritisation work than any scoring system I have used. A critical-rated flaw behind two authentication layers in an internal tool is genuinely less urgent than a medium-rated one on the signup page, and a scanner cannot tell you that because it does not know your topology. The second habit is to look for the class rather than the instance — when I find an unescaped output, I stop and ask why the framework's escaping was bypassed there, because the answer usually reveals a pattern that exists in forty other places. Fixing the one I found is the least valuable version of that work. And I write down accepted risks with a name against them, not out of bureaucracy but because the difference between "we accepted this" and "we never noticed" is invisible in the code and decisive in the post-mortem.
Model your own system
Set the capacity low. Severity-only ranking spends a fix on a deserialisation flaw in dead code — a genuine, critical-rated vulnerability with no path to it — while a missing rate limit on login goes untouched.
The Morris Worm of November 1988 is usually remembered as the first internet worm and is more interesting as the first time the industry had to reason about scale. It exploited three things — a buffer overflow in fingerd, a debug mode left enabled in sendmail, and weak passwords — and none of those was novel; all three were known. What was novel was that a single program could combine them and reach roughly ten percent of the internet in a day, because the internet had just become large enough for automation to matter. Robert Morris said he intended to measure the network's size, and the worm's damage came from a re-infection bug rather than a payload. Two things came out of it that still shape the field: CERT was founded weeks later, creating the idea of coordinated vulnerability response, and the industry learned that the relevant question about a flaw is not how clever it is but how reachable and how automatable it is. A debug mode nobody turned off did as much work as the overflow.
A team I advised had 1,200 open findings from their scanner and a policy that all "high" and "critical" items were fixed within 30 days. In practice the policy was quietly unenforceable, so nothing was fixed on schedule and everyone had stopped looking at the queue — the classic outcome of a target nobody can hit. We spent two days doing one thing: tagging each finding with whether an unauthenticated internet user could reach the code. That single attribute cut the actionable list from 1,200 to 94. Of those, eleven were in the payment and authentication paths, and the team fixed all eleven in a fortnight. What made it work was not a better scoring model — it was that the queue became small enough to believe in. The lead's summary was the useful part: their problem had never been prioritisation ability, it was that a list of 1,200 items communicates "this is hopeless" and a list of 94 communicates "this is a fortnight", and the same people responded completely differently to the second framing.
Take your own system and write down every place untrusted input crosses into trusted code. Not the ones you defend — all of them. Include the ones that feel like cheating: the support tool that can impersonate a user, the CI pipeline with production credentials, the webhook from a partner you have never met, the analytics script a marketer added. Then count. My expectation is that the number is two to three times what you would have guessed, and that at least one entry has never been reviewed by anyone because it did not feel like part of the application. That gap between the system you think you are defending and the system that actually exists is where most breaches live — and it costs an afternoon to close, because you cannot threat-model a component you have not listed.
Common Mistakes
Treating security as binary. There is no secure system, only one where attacking costs more than it yields — and both terms change over time.
Asking "is it secure?" Secure against whom, protecting what, at what cost to them. Three variables, no answer without them.
Only defending prevention. Three of the five levers reduce what an attacker gains or raise their odds of being caught.
Under-defending integrity. A leaked read is embarrassing; a silent write is trusted by everything downstream.
Forgetting authenticity. The fourth property, and what most authentication and signing work actually protects.
Treating review as a control. At an optimistic 85% coverage it still misses about one path of seven — it is a filter with a known miss rate.
Fixing instances instead of classes. One bug removes one path; parameterised queries end SQL injection.
Ranking by severity alone. A critical flaw in dead code scores 0; a minor one on the login page outranks it.
Dismissing findings with "it's behind a login". That assumes every authenticated user is trusted — exactly the assumption Part IV dismantles.
Confusing accepting risk with ignoring it. The difference is whether someone decided and wrote it down, and after an incident it is the whole conversation.
Leaving humans off the entry-point list. Support tooling and a plausible story defeat the strongest technical design.
Interview Corner
List every place untrusted input crosses into trusted code in a system you own — including support tooling, CI, webhooks and third-party scripts — and count them; the number will exceed your guess. Then take your current finding list and add one column: can an unauthenticated internet user reach this? Sort by that and watch the list collapse. Next, pick your highest-risk finding and ask whether the fix removes an instance or a class; if it is an instance, find out why the class-level defence was bypassed there, because it will be bypassed elsewhere too. Finally, find one risk your team has effectively accepted and write it down with a name and a date against it — the act of writing it is the difference between accepting and ignoring.
Run to execute the tests…
Summary
- Security is economic, not binary. No system is secure; some are not worth attacking, and both terms move over time.
- "Is it secure?" has no answer without: against whom, protecting what, at what cost to them.
- Four properties: confidentiality, integrity, availability — and authenticity, the one usually omitted.
- Integrity is under-defended. A leaked read is embarrassing; a silent write is believed by everything downstream.
- Three of five levers are not prevention — reduce the reward, raise detection, raise attribution.
- The asymmetry is 7:1 in a small application, and the failure asymmetry is worse: the attacker may fail infinitely, you may not fail once.
- Review is a filter with a miss rate, not a control — 85% coverage still leaves about one path of seven.
- Fix classes, not instances. Parameterised queries end SQL injection; they do not fix one.
- A vulnerability is not a risk. Impact × reachability: a critical flaw in dead code scores 0.
- Four-step chain: a flaw exists, it can be reached, reaching it matters, someone is motivated.
- "Behind a login" is not a control — it assumes every authenticated user is trusted.
- Accepted risk and ignored risk differ only in whether someone decided and wrote it down.
- Humans are entry points. Support tooling and a plausible story beat the strongest technical design.
In the next chapter, Threat Modelling, we make this systematic — a repeatable way to sit down with a design and produce the list of things that can go wrong, before anyone writes the code.
The team with 1,200 findings is the story I want to open this volume with, because their problem was not a security problem at all. They knew how to fix vulnerabilities; they had a queue that communicated hopelessness, and a policy that could not be met, and the entirely rational response to both is to stop looking. One attribute — can an unauthenticated user reach this — turned an impossible list into a fortnight of work, and nothing about their capability changed. I have come to think this is the most common failure mode in security engineering, and it is not technical: the field generates enormous volumes of undifferentiated findings, and undifferentiated findings are indistinguishable from noise regardless of how real each one is. The skill this volume is trying to build is therefore less about knowing more vulnerabilities and more about being able to look at a system and say which handful actually matter — because a team that can do that will fix the eleven that count, and a team that cannot will fix none of the twelve hundred.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.