The short version
SQL injection is when an attacker sends malicious SQL code through your application's inputs, and your database executes it. They can read your entire database, modify data, delete tables, or bypass authentication entirely.
It's tracked as CWE 89 and has been in the OWASP Top 10 since the list was created. After all these years, it's still everywhere because developers keep building queries the wrong way.
How it works
Say you have a login form. Your server side code builds a query like this:
// DO NOT DO THIS - vulnerable code
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
A normal user types user@example.com and the query works fine. But an attacker types this as the email:
' OR '1'='1' --
Your query becomes:
SELECT * FROM users WHERE email = '' OR '1'='1' --' AND password = ''
The -- comments out the password check. '1'='1' is always true. The attacker just logged in as the first user in your database, usually the admin.
That's the simple version. It gets worse. With UNION SELECT, an attacker can read any table. With DROP TABLE, they can destroy your data. With INTO OUTFILE, they can write files to your server.
Why AI tools keep generating vulnerable code
This is the part that matters if you're vibe coding. Ask ChatGPT, Copilot, or most AI tools to "build a login system with Node.js and MySQL" and there's a solid chance you'll get something like this:
// AI-generated code that looks clean but is vulnerable
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const result = await db.query(
`SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`
);
if (result.length > 0) {
res.json({ success: true, user: result[0] });
}
});
It reads well. It works in demos. It has zero protection against SQL injection. The AI gave you code that looks professional but hands your database to anyone who knows how to type a single quote.
The problem is that LLMs pattern match from training data that includes millions of tutorials with string concatenation. The "fix" is usually in a separate section the AI doesn't always include.
The fix: parameterized queries
Parameterized queries separate the SQL structure from the data. The database knows which parts are code and which parts are values. No amount of creative input can break out of a value position.
// Safe - parameterized query
const result = await db.query(
'SELECT * FROM users WHERE email = ? AND password = ?',
[email, password]
);
That's it. The ? placeholders tell the database driver to treat those values as data, never as SQL code. Even if someone types ' OR '1'='1' --, it gets treated as a literal string, not SQL.
In PostgreSQL with pg:
const result = await pool.query(
'SELECT * FROM users WHERE email = $1 AND password = $2',
[email, password]
);
ORMs: the safer default
If you're using Prisma, Drizzle, or any modern ORM, their standard query methods use parameterized queries automatically:
// Prisma - safe by default
const user = await prisma.user.findUnique({
where: { email: email }
});
This is one reason ORMs are recommended for vibe coders. You get SQL injection protection without thinking about it.
But watch out for raw queries. Every ORM has an escape hatch:
// Prisma - this is UNSAFE
await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${email}'`);
// Prisma - this is SAFE
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
The difference is subtle. One uses a tagged template literal that parameterizes automatically. The other uses a regular string. If you don't know the difference, use the ORM's standard methods and don't touch raw queries.
Beyond queries: defense in depth
Parameterized queries are the primary defense, but layer up:
- Validate input. An email field should look like an email. A numeric ID should be a number. Reject anything that doesn't match before it goes near a query.
- Use least privilege database accounts. Your app's database user shouldn't have DROP TABLE or FILE permissions. If the app only needs SELECT, INSERT, and UPDATE on specific tables, configure it that way.
- Use an ORM for standard operations. Save raw queries for the rare cases where the ORM can't express what you need, and parameterize those too.
- Don't expose database errors. A detailed SQL error message tells an attacker your database engine, table structure, and column names. Show generic errors to users, log details server side.
The real cost
SQL injection isn't theoretical. The Heartland Payment Systems breach exposed 130 million credit card numbers through SQL injection. The attack on TalkTalk in 2015 compromised 157,000 customer records.
For a solo developer or small startup, a SQL injection vulnerability means an attacker can dump your users table, including emails, hashed passwords, and whatever else you're storing. If you're not hashing passwords (another common AI generated code problem), it's game over for your users.
Check your site
Want to know if your site has this issue? Scan it now and find out in 60 seconds.
Frequently Asked Questions
- What is SQL injection in simple terms?
- SQL injection is when an attacker types specially crafted text into a form field or URL parameter, and your application passes it directly into a database query. The database executes the attacker's commands as if they were part of your code.
- Does using an ORM prevent SQL injection?
- ORMs like Prisma, Sequelize, and SQLAlchemy use parameterized queries under the hood, so their standard methods are safe. But if you use raw query methods like Prisma.$queryRawUnsafe() or Sequelize.query() with string concatenation, you're vulnerable again.
- Can SQL injection happen with NoSQL databases?
- Yes. NoSQL injection is a thing. MongoDB, for example, is vulnerable when you pass unvalidated user input into query operators like $gt or $ne. The concept is the same: untrusted input treated as code.
Your AI writes the code. We find what it missed.
Paste your URL. Security audit in 60 seconds.
Scan my app