SQL injection explained: how it works and how to stop it
SQL injection lets attacker input become part of your query. The fix is simple and well known: never build SQL from strings.

SQL injection (SQLi) happens when an application builds a SQL query by pasting user input into the text, so the input can change the meaning of the query. It has been on the OWASP Top 10 for years, and it is still found in real systems.
A vulnerable example
// DANGEROUS: string concatenation
const sql = "SELECT * FROM users WHERE email = '" + email + "' AND password = '" + pass + "'";
If a user types ' OR '1'='1 as the email, the query becomes:
SELECT * FROM users WHERE email = '' OR '1'='1' AND password = '...'
The condition is always true, and the login check can be bypassed. The attacker's text was treated as SQL code, not as data.
What an attacker can do
Depending on the bug and the database permissions: bypass login, read other users' data, modify or delete data, and in some setups reach the server. Data breaches often start here.
Types
- In-band: results or errors come back in the normal response (error-based, UNION-based).
- Blind: no data is shown, but the attacker infers answers from true/false page differences or timing.
- Second-order: malicious input is stored safely at first, then used unsafely in a later query.
The real fix: parameterized queries
Send the SQL and the data separately. The database never treats the data as code.
const row = await db.query(
"SELECT * FROM users WHERE email = $1 AND password_hash = $2",
[email, hash]
);
This works in every language (prepared statements, placeholders ? or $1). ORMs use it by default.
Common mistakes
- Raw queries inside an ORM. Building raw SQL with string interpolation brings the bug back.
- Dynamic parts that cannot be parameterized, such as table names, column names or
ORDER BYdirection. Use an allow-list: map the user's choice to a fixed set of known values. - Escaping by hand. It is easy to get wrong. Use parameters.
- Stored procedures that build SQL strings inside.
Defence in depth
- Least privilege: the app's database account should not be able to drop tables or read other schemas.
- Validate input (type, length, format). It helps, but it is not a replacement for parameters.
- Hide database errors from users. Log them privately.
- A WAF can reduce risk, but does not fix the code.
- Keep libraries updated, and scan code (static analysis) for string-built queries.
Testing safely
Test only systems you own or have written permission to test. A local practice app such as DVWA, or your own staging copy, is a safe place to learn. Never test on real sites without authorisation.
Takeaway: use parameterized queries everywhere, allow-list the dynamic parts, give the app a low-privilege database user, and hide error details.
Web Security series, Part 1 (injection and access control): 1 of 4.

