SQL injection
SQL injection is an attack in which an attacker's input is interpreted by the database as part of a SQL command instead of plain data, letting them read, change or delete records they should not reach. It happens when queries are built by joining strings. OWASP lists prepared statements with parameterised queries as the first defence.
How SQL injection happens
The bug appears when your code builds a query by pasting user input into a string. The database receives one block of text and cannot tell which part you meant as data. This is typical of code that was written quickly or generated without review:
# vulnerable: never build SQL from user input
email = request.args["email"]
cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
If a visitor submits x' OR '1'='1 as the email, the condition becomes true for every row and the query returns all users. Depending on the query and the database account's permissions, an attacker can also read other tables, change data or delete it. The OWASP SQL Injection Prevention Cheat Sheet describes the defences, and the same idea inspired the name of prompt injection.
The fix: parameterised queries
Send the SQL text and the values separately. The database then treats the values purely as data, whatever characters they contain. OWASP puts prepared statements with parameterised queries first among the defences, and warns that joining untrusted values into the query before preparing it defeats the protection.
# Python (psycopg)
cur.execute("SELECT * FROM users WHERE email = %s", (email,))
// Node.js (node-postgres)
const res = await client.query('SELECT * FROM users WHERE email = $1', [email]);
The psycopg documentation says never to use % or + to merge values into queries, and the node-postgres documentation warns that concatenating parameters into the query text can lead to SQL injection. ORMs are safe when you pass values as parameters. The usual trap is their raw query option, where OWASP says to bind untrusted values instead of concatenating them.
What parameters cannot cover
Placeholders stand for values, not for table names, column names or keywords such as ASC and DESC. When a user picks a sort order, map the choice to fixed text in your code and reject anything unknown:
const orders = { newest: 'created_at DESC', oldest: 'created_at ASC' };
const order = orders[req.query.sort] ?? orders.newest;
OWASP calls escaping all dynamic input strongly discouraged and recommends parameterised queries instead. It also stresses that binding values does not decide whether the caller may access the matching record, so you still need access rules such as row-level security.
Finally, limit the damage. OWASP warns against giving application accounts DBA or admin access and advises a separate database user per application with only the rights it needs. That does not prevent injection but makes a successful one far less costly. See least privilege.
Checking an AI-written app
Assistants can write correct parameterised code, but they can also produce string-built queries when asked to make something work quickly. A short review:
- Search the code for SQL built from variables. A rough first pass:
grep -rniE 'f"(select|insert|update|delete)|`(select|insert|update|delete)' src/
- Ask the assistant to list every query that uses string formatting, concatenation or template literals, and convert each to parameters.
- Type a single quote into your own forms and URL parameters. A database error or changed behaviour is a warning sign. Test only apps you own.
- Connect with a restricted database user, not the owner or an admin.
- Never pass model output straight into SQL; see the vibe coding security checklist.
A WAF can block some obvious attack patterns, but treat it as a safety net, not as a replacement for fixing the query.
Related terms: WAF, Least privilege, Row-level security (RLS), Prompt injection, Vibe coding.
Frequently asked questions
What is SQL injection?
It is an attack where input from a user is inserted into a database query in a way that the database runs it as SQL code. It can expose, change or delete data the attacker was never meant to touch.
How do I prevent SQL injection?
Use prepared statements with parameterised queries, which OWASP lists as the first defence. Keep table and column names fixed or chosen from an allow-list, and give your app a database account with minimal rights.
Do ORMs protect me from SQL injection?
Mostly, as long as you pass values as parameters. The risk returns when you use a raw query feature and concatenate untrusted input into the string, which OWASP tells you to avoid.
Can a WAF stop SQL injection?
A WAF can block some common attack patterns, but it is a safety net. The real fix is in your code: use parameterised queries so the database never treats input as SQL.
How can I test my own app for SQL injection?
Search the code for queries built from strings, then try a single quote in form fields and URL parameters of an app you own. Errors or odd behaviour suggest an unsafe query. Never test systems you do not own.
Want to see where your own server stands?
Run the free, read-only server check, or open the Security Lab and watch the detect, contain, recover, verify loop in action.
Get your free server checkOpen the Security Lab