Learn / Security / Lesson 05
Injection and escaping
SQL injection and cross-site scripting are the same mistake in two places, data being read as code, and they have the same cure.
Last updated: 2026-09-19
What it is
Injection happens when text from a user is glued into something that gets executed: a SQL query, an HTML page, a shell command. If the text contains the right characters, it stops being data and starts being instructions.
- In SQL it is SQL injection.
- In HTML it is cross-site scripting (XSS): the user's text becomes a
<script>that runs in other people's browsers.
Why it is a rule
- Heartland Payment Systems (2008). A SQL injection on a web form was the way in. The attackers went on to steal around 130 million card numbers, one of the largest card breaches ever recorded.
- TalkTalk (2015). A UK telecom lost the details of about 157,000 customers to a SQL injection on old web pages. The regulator fined them £400,000 and called the attack one that could have been prevented.
- The Samy worm (2005). An XSS in MySpace profiles. It spread to over a million profiles in less than a day.
How to do it
SQL: parameters, always
db.query(`SELECT * FROM users WHERE email = '${email}'`)
db.query('SELECT * FROM users WHERE email = $1', [email])The first line is the bug. The second sends the query and the value separately, so the database never reads the value as SQL, whatever it contains. There is no safe way to build the first one by "cleaning" input.
Table and column names cannot be parameters. When one must vary, pick it from a fixed list in the code, never from the request.
HTML: escape on output
Every piece of user text that goes into HTML is escaped for the place it lands.
const ESC = { '&': '&', '<': '<', '>': '>', '"': '"' }
const escape = (s) => String(s).replace(/[&<>"]/g, (c) => ESC[c])- Between tags and inside a quoted attribute, this is enough.
- Inside a
<script>, ahref, astyleor an unquoted attribute it is not. Keep user data out of those, or use an encoder made for that context. - Frameworks that escape by default (React, most template engines) are safe until someone uses the escape hatch (
dangerouslySetInnerHTML,{{{ }}},innerHTML).
Escape first, then format
A Markdown or rich-text renderer should escape the whole source before turning ** into <b>. Then a <script> typed into the source is already harmless text by the time any pattern sees it. Doing it in the other order turns the renderer into an injection door.
Links need a scheme check
<a href="javascript:…"> runs code when clicked, and escaping does not change that. Allow only https:, http:, mailto: and relative links.
Content-Security-Policy as the backstop
A CSP header tells the browser which scripts may run. If an injection gets through, a strict CSP can stop the script from executing. It is a second wall, not a replacement for escaping.
How we do it here
Every database call is parameterised. Every page renderer escapes before it formats, and link targets are limited to safe schemes. This site's Markdown renderer, the one drawing this page, escapes the whole source first.
Benefits
- Parameterised queries remove SQL injection completely, not partly.
- Escaping on output protects every page that shows the data, including ones written later.
- Both are cheap: no measurable cost.
Disadvantages
- Escaping has to be right for each context, and the HTML-body escape is wrong inside scripts and URLs.
- Dynamic SQL (sort columns, optional filters) needs allow-lists written by hand.
- A strict CSP breaks inline scripts and many third-party widgets until they are reworked.
Checklist
- No string-built SQL anywhere.
- User text is escaped at the point it enters HTML.
- Renderers escape before they format.
- Links allow only safe schemes.