How-To Guide
How to Prevent SQL Injection
The most important thing to get right in database security
TL;DR
TL;DR
Never put user input directly into SQL strings. Always use parameterized queries or an ORM like Prisma/Drizzle. If you're using template literals to build SQL queries, you're probably vulnerable.
What is SQL Injection?
SQL injection happens when user input becomes part of your SQL query. An attacker can manipulate the input to change what the query does.
Vulnerable Code
// NEVER DO THIS
const query = `SELECT * FROM users WHERE email = '${email}'`;
// What if email is: ' OR '1'='1
// Query becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
// This returns ALL users!
The Fix: Parameterized Queries
Node.js with pg (PostgreSQL)
Safe Code
// Parameters are escaped automatically
const result = await client.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
Node.js with mysql2
Safe Code
// Use ? placeholders
const [rows] = await connection.execute(
'SELECT * FROM users WHERE email = ?',
[email]
);
Prisma ORM
Safe Code
// Prisma handles parameterization automatically
const user = await prisma.user.findUnique({
where: { email: email }
});
// Even raw queries are safe with tagged templates
const users = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${email}
`;
Drizzle ORM
Safe Code
// Drizzle parameterizes automatically
const user = await db.select().from(users)
.where(eq(users.email, email));
// Raw SQL with sql template tag
const result = await db.execute(
sql`SELECT * FROM users WHERE email = ${email}`
);
Supabase
Safe Code
// Supabase client parameterizes automatically
const { data } = await supabase
.from('users')
.select('*')
.eq('email', email);
What About Dynamic Queries?
Sometimes you need dynamic column names or table names. These can't be parameterized.
Allowlist Pattern
const ALLOWED_COLUMNS = ['name', 'email', 'created_at'];
const ALLOWED_DIRECTIONS = ['ASC', 'DESC'];
function buildOrderBy(column, direction) {
// Validate against allowlist
if (!ALLOWED_COLUMNS.includes(column)) {
throw new Error('Invalid column');
}
if (!ALLOWED_DIRECTIONS.includes(direction)) {
throw new Error('Invalid direction');
}
// Safe to use because we validated
return `ORDER BY ${column} ${direction}`;
}
Common Mistakes
String Interpolation
Dangerous
// Template literals DON'T protect you
const query = `SELECT * FROM ${table} WHERE id = ${id}`;
// String concatenation is just as bad
const query = "SELECT * FROM " + table + " WHERE id = " + id;
Only Validating Some Inputs
Dangerous
// You parameterized email but not orderBy
const query = `SELECT * FROM users WHERE email = $1 ORDER BY ${orderBy}`;
// orderBy could be: id; DROP TABLE users; --
Escaping Instead of Parameterizing
Not Recommended
// Escaping is error-prone and can miss edge cases
const escaped = email.replace(/'/g, "''");
const query = `SELECT * FROM users WHERE email = '${escaped}'`;
// Just use parameterized queries instead!
Testing for SQL Injection
Try these inputs in your forms/API:
' OR '1'='1'; DROP TABLE users; --' UNION SELECT * FROM users --
If any of these cause errors or unexpected behavior, you have a vulnerability.
Quick Reference
| Library | Safe Pattern |
|---|---|
| pg (Postgres) | query('...WHERE x = $1', val) |
| mysql2 | execute('...WHERE x = ?', val) |
| Prisma | prisma.user.findMany({ where: {...} }) |
| Drizzle | db.select().where(eq(col, val)) |
| Supabase | supabase.from('x').eq('col', val) |