Sql Injection
By the end of this lesson you'll understand exactly how SQL injection turns user input into runaway SQL — and you'll be able to shut it down with parameterised queries , allow-lists, and least-privilege accounts. This is the single most important security skill in all of databases.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free SQL course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
The attacks in this lesson target a login that reads from this users table. Notice that row 1 is the admin — that matters, because a bypass logs the attacker in as the first matching row.
SQL injection happens when user input gets glued directly into a query string, so the database can't tell your code apart from their text . It's the top web vulnerability on the OWASP list — and it's 100% preventable.
Imagine dictating a form letter to an assistant: "Dear ___, ...". You expect a name. But the visitor says "Bob. Also, shred every file in the cabinet." If your assistant blindly writes down everything, the extra sentence becomes an instruction. A concatenated query is that gullible assistant — and the quote character ' is how the attacker ends the "name" and starts dictating commands.
Below is the vulnerable pattern. The nameInput isn't a value to the database — once it's pasted into the string, it's part of the SQL itself.
Now watch what a single quote does. The attacker's text closes the string early, adds OR '1'='1' (true for every row), and uses -- to comment out the rest of your query.
The same hole lets an attacker stack a whole new statement after a semicolon — including a destructive one like DROP TABLE — or bolt on a UNION SELECT to siphon data out of an unrelated table.
Fill in the blanks to explain the flaw and to show exactly what input breaks this concatenated query. The expected answer is in the comments so you can self-check.
A parameterised query (also called a prepared statement ) splits the SQL from the data. You write the query with a placeholder — ? , $1 , or :name depending on your engine — and hand the value over separately. The database compiles the query first , then slots your value in as pure data. Quotes in the input can no longer "escape" into the SQL, because the SQL is already finished.
Want to see the separation? PostgreSQL exposes it directly with PREPARE and EXECUTE : the plan is built once, and every value you pass afterwards is bound into a typed hole.
Take the same vulnerable login and make it safe by replacing the concatenation with a parameter placeholder. One blank — use ? (or $1 / :email ).
Parameters are the fix — but good security stacks layers, so a single mistake isn't fatal. The catch: you can never bind a table or column name as a parameter. For those, validate against an allow-list (a fixed set of names you accept). Escaping input by hand is a brittle last resort — get it wrong once and you're exposed.
Assume an attack might one day slip through. The login your app connects with should only be able to do the app's job — no DROP , no ALTER , no superuser. Then even a successful injection runs into permission denied for the worst operations.
An ORM (Object-Relational Mapper — the library that turns objects into SQL, like Django, SQLAlchemy, ActiveRecord, or Prisma) parameterises for you by default, which is a big part of why ORMs are safer. But every ORM has a raw SQL escape hatch, and the moment you use it you're back to manual safety. Stored procedures aren't automatically safe either: if a procedure builds SQL from concatenated text and runs it with EXECUTE , it's just as injectable.
Placeholder style varies ( ? , $1 , :name , %s , @id ) but the rule is the same everywhere: SQL with a placeholder, value passed separately.
Q: If I parameterise everything, do I still need input validation?
Yes — as a second layer. Parameters stop injection in values , but they can't protect a table or column name you build dynamically, and validation enforces business rules (length, format). Use both; never let validation be your only defence.
Don't. Hand-escaping is notoriously easy to get wrong — character sets, Unicode look-alikes, and numeric contexts all create holes. A parameterised query is simpler and safer, so reach for escaping only as a genuine last resort.
Q: Are stored procedures automatically safe from injection?
No. A procedure that builds a query by concatenating strings and runs it with dynamic EXECUTE / EXEC is just as injectable as application code. Use parameters inside the procedure too ( EXECUTE ... USING ).
Q: My ORM builds the SQL for me — am I covered?
Mostly. ORMs parameterise normal queries by default, which is great. The risk is the raw SQL escape hatch: an f-string or string concatenation passed to .raw() / .query() bypasses that protection. Always pass values as parameters, even in raw queries.
Put it all together — a brief, a blank canvas, and the expected shape in the comments. Make the two-field login injection-proof, then sanity-check it in a playground.
Practice quiz
What root cause makes SQL injection possible?
- Weak passwords
- Slow networks
- User input is concatenated into the SQL string, so the database can't tell code from data
- Too many indexes
Answer: User input is concatenated into the SQL string, so the database can't tell code from data. Injection happens when user text is glued into the query string and becomes part of the SQL code, not just a value.
Why does the input ' OR '1'='1 bypass a login?
- '1'='1' is true for every row, so the WHERE matches everyone
- It guesses the password
- It deletes the users table
- It encrypts the query
Answer: '1'='1' is true for every row, so the WHERE matches everyone. The injected condition is always true, so the WHERE returns every row and logs the attacker in as the first one.
What is the #1 fix for SQL injection?
- Hand-escaping quotes
- Hiding error messages
- Renaming tables
- Parameterised queries (prepared statements)
Answer: Parameterised queries (prepared statements). Parameterised queries split the SQL from the data: a placeholder in the query, the value passed separately as data.
In a parameterised query, how is the placeholder written for a value?
- With quotes around it, like '?'
- With no quotes around it, like ?
- As a comment
- Always as a number
Answer: With no quotes around it, like ?. The placeholder (?, $1, :name) takes no quotes; the driver binds the value, so writing '?' creates a literal question mark.
Why can't a parameter protect a table or column name in dynamic SQL?
- Parameters bind only values, not identifiers
- Names are too long
- It works fine, they can
- Only numbers can be parameters
Answer: Parameters bind only values, not identifiers. You can never bind a table or column name as a parameter; for identifiers you must validate against an allow-list.
What does a least-privilege database account achieve against injection?
- It prevents all injection
- It speeds up queries
- It limits the blast radius so a successful injection can't, e.g., DROP tables
- It replaces parameterised queries
Answer: It limits the blast radius so a successful injection can't, e.g., DROP tables. Least privilege caps the damage (no DROP/ALTER), but it doesn't prevent injection or stop row leaks; pair it with parameters.
Are stored procedures automatically safe from SQL injection?
- Yes, always
- No, if they build SQL from concatenated text and run it with EXECUTE they are injectable
- Yes, because they're in the database
- Only on PostgreSQL
Answer: No, if they build SQL from concatenated text and run it with EXECUTE they are injectable. A procedure using dynamic EXECUTE on concatenated strings is just as injectable; use parameters/USING inside procedures too.
How do ORMs help with injection by default?
- They block all SQL
- They encrypt every query
- They disable the database
- They build parameterised SQL for you automatically
Answer: They build parameterised SQL for you automatically. ORMs parameterise normal queries by default, but their raw-SQL escape hatch bypasses that protection if you concatenate.
Why is hand-escaping quotes a poor primary defence?
- It is too fast
- Encodings, Unicode, and numeric contexts can slip past it, so it's fragile
- It only works on MySQL
- It requires a parameter anyway
Answer: Encodings, Unicode, and numeric contexts can slip past it, so it's fragile. Manual escaping is notoriously easy to get wrong; a parameterised query is both simpler and safer.
Why is client-side (browser) validation not a security control?
- It is too slow
- It uses too much memory
- Attackers can call your API directly (e.g. with curl), bypassing it
- It only validates numbers
Answer: Attackers can call your API directly (e.g. with curl), bypassing it. JavaScript checks are a UX nicety; you must validate and parameterise on the server because the client can be bypassed.