SQL Injection Explained: The Classic Attack and the One-Line Fix
SQL injection lets an attacker rewrite your database queries by typing into a form. How it works with a simple example, what damage it can do, why parameterized queries and ORMs prevent it, the places AI-generated code still gets it wrong, and how to check your app.
SQL injection is one of the oldest attacks on the web, and it still shows up in new apps — including AI-generated ones. It's worth understanding because the cause is simple, the damage can be total, and the fix is easy.
How it works
Your app builds a database query using something the user typed. Say, a login check:
const query = `SELECT * FROM users
WHERE email = '${email}' AND password_hash = '${hash}'`
A normal user types [email protected], and the query is exactly what you'd expect.
Now an attacker types this into the email field:
' OR '1'='1' --
The query becomes:
SELECT * FROM users
WHERE email = '' OR '1'='1' --' AND password_hash = '...'
- The
'closes the email string early. OR '1'='1'is always true, so it matches every user.--starts an SQL comment, so the rest of the query — the password check — is ignored.
The attacker is logged in as the first user in the table, often an admin. The user's input stopped being data and became part of the command. That's injection.
What an attacker can do
Depending on the query and the database permissions:
- Log in as anyone without a password.
- Read the entire database — every user's email, personal data, and password hashes.
- Change or delete data — including
DROP TABLE. - In some setups, go further into the server.
It has been behind some of the largest data breaches on record.
The fix: parameterized queries
The fix is to never glue user input into SQL text. Instead, send the query and the values separately, using placeholders. The database then treats the values purely as data, no matter what characters they contain.
// ✅ Parameterized — safe
const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[email]
)
With the attacker's input, the database simply looks for a user whose email is literally ' OR '1'='1' --. There isn't one. Attack over.
Every language and database library supports this. Placeholders look like $1, ?, or :email depending on the library.
ORMs protect you — mostly
ORMs like Prisma, Drizzle, SQLAlchemy, and Django's ORM parameterize queries for you:
await prisma.user.findUnique({ where: { email } }) // safe
That's a big reason to use one. (What Is an ORM?.) But every ORM also has escape hatches for raw SQL, and that's where injection sneaks back in.
Where AI-generated code still gets it wrong
Watch for these patterns in your code — and ask your AI tool to look for them:
String-built raw queries:
// ❌ Template string pasted into SQL
await db.query(`SELECT * FROM plants WHERE name LIKE '%${search}%'`)
// ✅ Parameterized
await db.query("SELECT * FROM plants WHERE name LIKE $1", [`%${search}%`])
Unsafe raw-query functions in ORMs. Prisma, for example, has $queryRaw (safe when used as a tagged template) and $queryRawUnsafe (dangerous with user input). Names with "unsafe" or "raw" deserve a close look.
Sorting and column names from the user. Placeholders work for values, not for column names or ASC/DESC. Code like ORDER BY ${req.query.sort} is injectable. Use an allowlist:
const sortable = { name: "name", date: "created_at" }
const column = sortable[req.query.sort] ?? "created_at"
Search and filter builders that assemble WHERE clauses from strings.
Database functions or stored procedures that build SQL dynamically inside the database.
Defence in depth
Parameterized queries are the fix. These reduce the damage if something slips through:
- Least-privilege database users. Your app's database account shouldn't be able to drop tables or read tables it doesn't need.
- Server-side input validation. Reject input that doesn't fit what you expect. (Form Validation Explained.)
- Don't show raw database errors to users. They help attackers map your database.
- Row-level security where your platform supports it, so even a bad query only sees one user's rows. (Supabase Row-Level Security Explained.)
How to check your app
Ask your AI tool:
Search the codebase for every database query built with string concatenation or template strings, every use of raw or unsafe query functions, and every ORDER BY or column name taken from user input. List each with the file and line, and whether user input can reach it.
Then fix each one with parameters or an allowlist. Automated security scanners and linters can help catch these too.
EasySpawn gives Claude Code a real backend and managed Postgres to work with, so it can audit your queries — and test the fixes — against an actual database rather than a guess. See how it works or join the waitlist.
Related: What Is XSS? · A Security Checklist for Vibe-Coded Apps · SQL for Beginners
Keep reading
Password Hashing Explained: Why You Never Store Passwords
A well-built app doesn't know your password — it stores a hash. What hashing is, why fast hashes like MD5 and SHA-256 are wrong for passwords, what salts do, why bcrypt and Argon2 exist, and how to check that your AI-built app got it right.
Backups for Beginners: How to Not Lose Your App's Data
Your code is in Git. Your users' data isn't. What actually needs backing up, the 3-2-1 rule, why a backup you've never restored doesn't count, how often to back up, and a simple backup plan for a small app — including what to do before letting an AI agent near your database.