The Bug Is the Way You Built the String
SQL injection is thirty years old and still shipping, because it is taught as a trick to defend against rather than as a mistake you can stop making. There is nothing to patch except how the query gets assembled.
Injection is the oldest bug on any list you care to read, and it is still being written this week, in new code, by people who have heard of it. That is worth being curious about rather than smug about. A bug this well known that keeps shipping is usually being taught wrong.
It is normally taught as an attack: here is a payload, here is what it does, do not let it happen. That framing puts you on defence against a string. The useful framing is the opposite — it is a mistake with one cause, and you can simply stop making it.
Data that becomes code
A program keeps data and code in separate worlds. A name is data; the instruction that looks the name up is code. Injection is what happens when data crosses the border into code — when something a stranger typed stops being treated as a value and starts being treated as an instruction.
It crosses that border for one reason, every time: the program built its instruction by gluing strings together, and the stranger's text was one of the pieces. Once their text is part of the instruction, anything in it that reads as an instruction *is* one.
How the bug is written
query = "select * from users where name = '" + input + "'"What somebody sends
input = "' OR 1=1 --"The application meant those quotes to wrap a value. The input closes the first quote early, writes a condition of its own, and comments out the rest. Nothing was hacked. The string was built to say something else.
Nothing malfunctioned
This is the part worth sitting with, and the part that changes how you read your own code. The injected query does not error. It does not crash the database or bypass a check. It is valid SQL, and the database executes it exactly as written, because that is the job.
' close the quote the application opened for you
OR 1=1 add a condition of your own that is always true
-- comment out whatever the application wrote next
- Line 2
1=1is true for every row, andORneeds only one side to be true. The filter the application wrote is now joined to a condition that always holds, so every row matches. Ordinary SQL, doing ordinary SQL things. - Line 3Two dashes begin a comment. Everything after the input — including the clause that was hiding unlisted rows — stops being part of the query at all.
No protection was defeated, because there was no protection. The query did what its text said, and somebody else wrote part of the text. There is nothing to patch except that.
The remedies that are not the fix
Injection attracts folk remedies, and leaning on them instead of the actual fix is how injectable code survives a review. Each of these gets proposed in every codebase eventually.
escaping quotes misses every input you forget to escape, and every
encoding trick that reaches the parser anyway
blocking keywords "OR", "SELECT" - breaks real data, blocks nothing an
attacker cannot spell around
a web firewall buys time, catches known payloads, and is not a fix -
it is a smoke alarm, not a fireproof wall
- Line 1Every one of these tries to make dangerous input safe. All three are filters, and a filter is a list of the attacks you thought of.
The keyword denylist deserves a special mention, because it fails in both directions at once. It does not stop anybody who can spell around it, and it breaks your genuine data the first time an O'Brien joins the company. When a defence costs real users and buys nothing, that is not a tuning problem. It is evidence the approach is wrong.
The fix, which is less code
Write the query with a placeholder and hand the value over separately. The database treats it as a value and only ever as a value, because it never reaches the parser as SQL.
The bug
query = "select * from users where name = '" + input + "'"
run(query)The cure
query = "select * from users where name = ?"
run(query, [input])The ? is a placeholder, not a quote. Now ' OR 1=1 -- searches for a user literally named "' OR 1=1 --", finds nobody, and changes nothing. The attack becomes an ordinary failed search.
Why can an attacker not break out of a parameter the way they broke out of the quotes?
Notice what the safe version costs: nothing. It is *less* code than building the string by hand — you delete the quotes and the plus signs. Every language's database library supports it. "No time to fix it" rarely survives contact with the diff.
The shape generalises, which is the real lesson
Note
Whenever you are about to build a command, a query, a path or a template out of a string with somebody else's input in it, stop. The bug is nearly always "I built the instruction as a string", and the fix is nearly always "hand the input to the thing as data".
Command injection, template injection, path traversal: same border, same cause, same cure in a different library. Learn the shape once on the simplest case and you will recognise it in code that looks nothing like this.
Legal
Everything in that course runs against targets we built to be broken, on a separate domain, with data that is not real. The same three keystrokes against a system you do not own or have written permission to test is a criminal offence in most countries. Finding a real bug is not a defence, and neither is meaning well.