The CWE/SANS Top 25, input validation, and safe coding
◈ 10 cardsOrganise the Top 25 into its three categories with a real incident for each, then work the four habits that prevent most of it: allowlist validation, canonicalisation before comparison, output encoding for the destination, and atomic creation instead of test-then-create.
Why software security is not software quality
Start with the sentence that organises the whole chapter. Software quality and reliability are concerned with accidental failure, under inputs drawn from some plausible probability distribution, and the fix is to test against that distribution until failures become rare. Software security is different because the attacker chooses the distribution. An attacker deliberately searches for inputs that trigger specific exploitable bugs, and those inputs typically look nothing like anything the designers anticipated — which is exactly why ordinary testing does not find them. "We tested it thoroughly" is a quality claim, not a security claim.
The corresponding discipline is defensive programming: writing software that keeps functioning under deliberate attack, detects erroneous conditions, and either continues safely or fails gracefully. Its governing rule is short. Never assume anything.
A useful mental model of any program: it reads input from many sources, processes it by some algorithm, interacts with the operating system and other programs, and produces output to several destinations. Four components, four questions — where does data come in, what do I do with it, what do I ask the system for, and where does it go? — and every section of this lesson answers one of them.
The Top 25, in its three categories
The CWE/SANS Top 25 is a ranked list of the most dangerous software weaknesses. Do not memorise twenty-five ranks. Learn the three categories, learn which weaknesses live in each, and learn the mechanism of the members that actually get exploited. An answer that groups and explains beats one that recites, every time.
Category 1 — insecure interaction between components. Weaknesses in how data is sent between components, modules, programs or systems. Members: cross-site scripting (#2), SQL injection (#3), improper input validation (#4), OS command injection (#6), cross-site request forgery (#9), unrestricted upload of a dangerous file type (#10), deserialization of untrusted data (#12), command injection (#17), server-side request forgery (#21), XML external entity processing (#24) and code injection (#25).
Category 2 — risky resource management. Weaknesses in how software creates, uses, transfers and destroys resources such as memory. Members: out-of-bounds write (#1), out-of-bounds read (#5), use after free (#7), path traversal (#8), NULL pointer dereference (#11), integer overflow or wraparound (#13), improper restriction of operations within a memory buffer (#19), race condition (#22) and uncontrolled resource consumption (#23).
Category 3 — porous defenses. Defensive techniques that are missing, misused or simply wrong. Members: improper authentication (#14), use of hard-coded credentials (#15), missing authorization (#16), missing authentication for a critical function (#18) and incorrect default permissions (#20).
Lead your answer with the headline fact: out-of-bounds write is number one and out-of-bounds read is number five. Everything the last four lessons covered still tops the list decades after it was first published.
Recent examples worth naming
A marker asking for "recent examples" wants named incidents with the mechanism attached, not brand names. Three, one per category:
- Code injection (#25, category 1) — Log4Shell, CVE-2021-44228 (2021). A widely deployed Java logging library expanded a lookup expression found inside a string it was asked to log. An attacker-supplied string caused the library to fetch and execute a class from a directory server the attacker controlled. The lesson is the general injection rule: data crossed into a position where it was interpreted.
- Out-of-bounds read (#5, category 2) — Heartbleed, CVE-2014-0160 (2014). A TLS heartbeat message carried its own length field, and the implementation echoed back that many bytes without checking against how many had actually arrived. The reply contained adjacent process memory, including private keys. Note it is an over-read, not an over-write: nothing was corrupted, and the impact was disclosure.
- Race condition (#22, category 2) — Dirty COW, CVE-2016-5195 (2016). A race in the Linux kernel's copy-on-write handling let a local user win a timing window and obtain write access to memory that should have been read-only — local privilege escalation from an ordinary account.
(One more for category 1 if you want a fourth: MOVEit Transfer, CVE-2023-34362 (2023), an SQL injection in a managed file-transfer product that was exploited at scale for data theft.)
All five CVE identifiers are outside the textbook and should be cited to the MITRE CVE records, not to the book.
Injection is one idea wearing four costumes
Injection weaknesses account for four separate Top 25 entries, and they are all the same failure: input data is placed where the receiving component expects code, so the component executes it. The discrimination that earns marks is which interpreter receives the metacharacters.
- OS command injection — shell metacharacters reach a shell. The sharp observation: a local user could always have run those commands, but with their own privileges. The vulnerability is that a web interface runs them with the server's privileges on behalf of an unknown outsider.
- SQL injection — SQL metacharacters reach a database engine.
- Code injection — scripting-language source reaches a language runtime. Distinct from the machine code of the earlier lessons.
- Cross-site scripting — script reaches another user's browser. XSS is the odd one out: your program is not the target, your users are. It works by subverting the browser's assumption that all content from one site is equally trusted, giving the attacker's script access to page contents and session cookies.
The general rule: injection can occur whenever one program passes externally sourced data to another program, service or function without sufficient validation.
Habit 1 — allowlist, not denylist
Validate input against what is acceptable, not against a catalogue of what is dangerous. A denylist enumerates known-bad values and is defeated the moment someone finds a representation you did not enumerate — and people find new ones continuously. An allowlist enumerates the acceptable form and rejects everything else, so a novel encoding fails by default rather than passing by default.
The honest trade-off is worth stating in an exam: an allowlist will reject some inputs that were actually fine, and that is the safe direction to fail in.
Habit 2 — canonicalise before you compare
A character can have several encodings, and comparison happens on bytes. The forward slash has a legal one-byte form and redundant longer UTF-8 encodings; strictly only the shortest form is valid, but many decoders historically accepted any equivalent sequence — which is precisely how path-traversal filters were bypassed on production web servers. So reduce input to a single, standard, minimal representation first, and validate that. Canonicalise then validate, never validate then canonicalise: the second order checks a string that no longer exists by the time the value is used.
Numeric input has its own version of this trap. A length field read as unsigned but compared as signed lets a very large value be interpreted as negative, pass a check against a smaller positive maximum, and then overflow when it is used as a size.
Habit 3 — encode on output, for the destination
The mirror image of canonicalisation. Users assume that everything they see was produced or validated by the program they are using — but a program may accept input from one user and display it to another, and if that content alters the behaviour of the displaying program the attack lands on the viewer. Filter output by what is safe rather than by what is dangerous, for the same reason as on input, and pin the output encoding explicitly — on the web, via the content-type header — because if the consumer assumes a different encoding than the producer did, the filtering is subverted.
Habit 4 — create atomically; never test then create
A lockfile is purely advisory: a program that ignores it is not stopped. Worse, the obvious implementation is a race. Check whether the file exists, find it does not, create it — and two processes can both pass the check before either creates. The check and the create must be one uninterruptible operation, so the correct implementation never tests separately: it attempts to create with a flag that fails if the file already exists, which the operating system performs atomically, and retries on failure.
Temporary files have the mirror-image requirement — unique, and untouchable by others. The common insecure recipe builds a name from the process ID, checks it does not exist, and creates it: the same race. An attacker who guesses the name and creates it inside the window, classically as a symbolic link to a sensitive file, has a privileged program follow the link and destroy the target. Secure practice is a random name, atomic creation, minimum access, and close-and-unlink when finished, with the sticky bit set on any shared temporary directory. (The general name for this class — time-of-check-to-time-of-use, TOCTOU — is not the textbook's term but is what you will meet everywhere else.)
And one habit that spans all four: least privilege
Because an exploit runs with the privileges of the compromised program, privilege escalation is the attacker's key step — so give a program only what it needs and only for as long as it needs it. A network server needs root only to bind a privileged port and can drop to an unprivileged account immediately afterwards. A privileged program should be able to modify only the files it actually needs; letting a web server own its entire document hierarchy is the direct cause of how common web defacement is. And partitioning a large program into small modules, each holding its privileges only while in use, keeps the privileged parts small enough to scrutinise — the difference between a mail system with a long record of security bugs and its privilege-separated replacement.