Intermediate · Backend security · topic 37 of 54 · 25 min
SQL injection and how to stop it
Explanation
How the attack works
An SQL query is a piece of text that the database reads as instructions. If your code builds that text by joining strings with user input, the user is now helping to write your instructions. That is injection: user input that becomes code.
// DANGER: do not write code like this
const rollNo = req.query.roll; // typed by the user
const sql = "SELECT name, marks FROM results WHERE roll_no = '" + rollNo + "'";
const rows = await db.query(sql);A normal student types R101 and gets one row. Now an attacker types this into the box: x' OR '1'='1 . Paste it into the string and look at the query the database receives.
-- The attacker typed: x' OR '1'='1
SELECT name, marks FROM results WHERE roll_no = 'x' OR '1'='1'
-- '1'='1' is always true, so EVERY row comes backThe real damage can be large: reading every user's email and password hash, logging in as admin without a password, changing prices, or deleting tables. SQL injection has caused many big data leaks.
The fix: parameterised queries
A parameterised query (also called a prepared statement) sends the SQL text and the values separately. The SQL has placeholders where values go. The database driver sends the values on their own, so they are always treated as data, never as code, whatever quotes they contain.
// node-postgres (package "pg"): placeholders are $1, $2, ...
const { rows } = await pool.query(
'SELECT name, marks FROM results WHERE roll_no = $1',
[req.query.roll]
);// mysql2: placeholders are ?
const [rows] = await connection.execute(
'SELECT name, marks FROM results WHERE roll_no = ? AND year = ?',
[req.query.roll, 2026]
);An ORM (a library that builds SQL for you, like Prisma, Sequelize or Drizzle) uses parameters for normal queries, which helps a lot. But every ORM also has a way to run raw SQL. Raw queries still need placeholders, and string-building there is just as dangerous.
| Do | Do not |
|---|---|
| Use $1 or ? placeholders for every value | Join user text into SQL strings |
| Use an allow-list for column names and sort order | Trust a column name from the URL |
| Validate input type and length too | Rely only on removing quotes by hand |
| Give the app user only the rights it needs | Connect as the database owner or superuser |
The same idea applies beyond SQL. NoSQL databases, shell commands and LDAP queries can be injected too. The rule is always the same: keep code and data apart.
Watch the lesson
SQL injection and how to stop it · 1/7
SQL injection is one of the oldest web attacks, and it still works on careless code.
Prefer a video by a teacher? Find free videos on “sql injection explained parameterized queries” on YouTube ↗
In real life: The login box that let anyone in
A small coaching institute built a login page with this query: SELECT * FROM users WHERE email = '<email>' AND password = '<password>'. Someone typed admin@example.in' -- as the email. In SQL, -- starts a comment, so the password check was ignored and they were logged in as admin. The fix was two changes: a parameterised query with placeholders, and storing only password hashes (checked with bcrypt) instead of plain passwords.
Practice (0/2 done)
See the danger for yourself. The results site runs: "SELECT name, marks FROM results WHERE roll_no = '" + input + "'". An attacker types: x' OR '1'='1 . Write the exact query the server ends up running (put the input inside the quotes) and run it. You should see EVERY student's marks.
Write buildQuery(filters) for a product search. It returns { sql, params } for PostgreSQL. Start from "SELECT id, name, price FROM products". Supported filters, always in this order: city (city = $n), category (category = $n), maxPrice (price <= $n). Join conditions with " AND " after " WHERE ". Ignore any other keys. Never put a filter value into the sql text.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.