SQL injection has leaked passwords, drained accounts, and brought down companies for three decades, and it remains near the top of every security list because it is so easy to write by accident. The bug appears the moment an application glues user input directly into a SQL string instead of treating it as data.
The danger is not that the user types something rude — it is that the input is parsed as code. A carefully placed quote can close the string the app intended and append a whole new clause the developer never wrote. The database cannot tell the developer's SQL from the attacker's, so it runs both. Understanding the mechanism is the first step to never writing it, and the defense is simpler than the attack.
flowchart LR I["attacker submits a password"] --> C["app pastes it into the SQL string"] C --> Q["finished string: password = '' OR '1'='1"] Q --> DB["the OR condition is always true"] DB --> R["every row matches — login bypassed"] style I fill:#b45309,color:#fff style R fill:#b45309,color:#fff
Why a quote is a weapon
Everything turns on the difference between code and data. When the application writes password = ' and then drops in the user's input and a closing quote, it has handed the database a finished string to parse. If the input contains a quote of its own, that quote terminates the string early — and whatever follows is parsed as fresh SQL.
Type the password ' OR '1'='1 and the finished clause reads password = '' OR '1'='1. The database sees an empty password OR a condition that is always true, so every row matches and the login succeeds with no correct password at all. The attacker never needed a password; they needed one quote and a tautology, and the application handed them the keyboard.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL
);
INSERT INTO users (username, password) VALUES
('admin', 's3cret-pw'),
('ada', 'ada-pw');
-- A correct password check returns one row:
SELECT username FROM users WHERE username = 'admin' AND password = 's3cret-pw';
-- The attacker submits this password: ' OR '1'='1
-- Pasted into the string, it becomes a tautology:
SELECT username FROM users WHERE username = 'admin' AND password = '' OR '1'='1';
The defense: parameters, not strings
The reliable fix is to never build SQL by pasting strings together. A parameterized query (also called a prepared statement) sends the query skeleton and the user's values to the database in two separate pieces. The skeleton has placeholders where the values go, and the database binds each value as pure data — it is never parsed as SQL, so a quote in the input is just a quote character inside a string, not a string terminator.
Because the code and the data travel separately, there is no moment for the input to break out and become a command. The same query runs regardless of what the user typed. This is why parameterization is the single recommended defense across every major database and language — it removes the vulnerability at the root rather than chasing each tricky input.
flowchart TD
subgraph VUL["Vulnerable: one concatenated string"]
S1["query text + user input"] --> P1["parsed together as SQL"]
end
subgraph SAF["Parameterized: two channels"]
S2["query skeleton with ?"] --> B["binder"]
D2["user values as data"] --> B
end
style VUL fill:#b45309,color:#fff
style SAF fill:#0e7490,color:#fff
Vulnerable vs safe, side by side
The vulnerable app builds one string by gluing the user's typing between quote characters, so the input is read as SQL. The safe app keeps the query text fixed with a placeholder and sends the user's value through a separate binding channel, where a quote is just a character inside a value.
In pseudo-code the two shapes look like this:
VULNERABLE q = 'SELECT name FROM users WHERE id = ' + input
SAFE q = run('SELECT name FROM users WHERE id = ?', input)
The ? is a slot the database fills with the bound value. Whatever the input contains — quotes, semicolons, a whole second statement — is matched literally against the column and can never become part of the command.
SELECT name, role FROM employees WHERE id = 3;
flowchart TD P["Parameterized queries"] --> L1["primary defense — code and data stay separate"] A["Least-privilege DB account"] --> L2["limits what a successful bug can touch"] W["Allowlist for dynamic names"] --> L3["guards the rare structural value"] style L1 fill:#0e7490,color:#fff
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER);
CREATE TABLE secrets (id INTEGER PRIMARY KEY, token TEXT);
INSERT INTO products (name, price) VALUES ('Widget', 500), ('Gadget', 900);
INSERT INTO secrets (token) VALUES ('super-secret-api-key');
-- A normal product search:
SELECT name, price FROM products WHERE name = 'Widget';
-- Attacker input: noname' UNION SELECT token, id FROM secrets --
-- The injected UNION pulls rows from a different table:
SELECT name, price FROM products WHERE name = 'noname' UNION SELECT token, id FROM secrets;
What does SQL injection let an attacker do?
Injection lets attacker-controlled text execute as SQL. Depending on the account's privileges, that can read secrets, modify or delete data, or even run admin commands — not just defeat a single login form.
Which technique reliably prevents SQL injection?
Parameterized queries send the query skeleton and the values in separate channels, so user input can only ever be data — there is no string for it to break out of. Escaping, hiding errors, and length limits are weak defenses that do not address the root cause.
A lookup by primary key is the canonical query an app should always parameterize. Write the SQL (as it reads with the value filled in) that returns the name and role of the employee whose id is 3.
SELECT -- name and role for the employee with id 3 FROM employees WHERE ;
Select the name and role columns.
Filter with WHERE id = 3.
SELECT name, role FROM employees WHERE id = 3;
Which snippet is vulnerable to SQL injection?
The second snippet concatenates user_id straight into the SQL string, so the input is parsed as code. The ? placeholders in the other snippets bind the value as data, which cannot break out into SQL.
An app lets staff search employees by role and must run the query parameterized. Write the SQL (value filled in) that returns only the name of every employee whose role is 'Researcher'.
SELECT -- names of Researchers FROM employees WHERE ;
Select only the name column.
Filter with WHERE role = 'Researcher' — note the single quotes around the text.
SELECT name FROM employees WHERE role = 'Researcher';
Recap
- SQL injection happens when user input is concatenated into a query string and parsed as code, not data.
- A single quote can terminate the intended string and append a clause like
OR '1'='1that is always true. - Parameterized queries send the skeleton and the values separately, so input can only ever be data — the root-cause fix.
- Manual escaping, hiding errors, and length limits do not reliably prevent injection.
- A lookup by id or by an exact value is exactly the query that must always be parameterized in real application code.
That closes the module: you can now read query plans, choose indexes, rewrite anti-patterns, design a sound schema, debug systematically, and write SQL that an attacker cannot turn against you.