Always use parameterized queries or prepared statements, never string concatenation. String-interpolated SQL is the #1 cause of SQL injection — an attacker can modify your query to read, modify, or delete your entire database.
SQL injection has been the #1 web vulnerability for over 20 years. When user input is concatenated into SQL strings, an attacker can terminate the intended query and inject their own SQL. This allows them to bypass authentication, read any table, modify any data, or drop the entire database. Parameterized queries prevent this by separating the SQL structure from the data values, making injection impossible by design.
BeforeMerge scans your pull requests against this rule and 4+ others. Get actionable feedback before code ships.
SQL injection occurs when user input is embedded directly into a SQL query string. The database cannot distinguish between the query structure and the user-supplied data, so an attacker can modify the query itself.
Consider a login query: SELECT * FROM users WHERE email = '${email}'. If the attacker submits ' OR 1=1 -- as the email, the query becomes:
SELECT * FROM users WHERE email = '' OR 1=1 --'This returns every user in the table, bypassing authentication. More destructive payloads can DROP TABLE, INSERT admin users, or COPY data to external servers.
Parameterized queries solve this by sending the query structure and data values separately. The database treats parameters as data, never as SQL structure, making injection impossible by design.
Never concatenate, interpolate, or template user input into SQL strings. Always use parameterized queries (also called prepared statements or bind variables) where the SQL structure and data values are separated.
// BAD: string interpolation — SQL injection vulnerability
async function getUser(email: string) {
const result = await pool.query(
`SELECT * FROM users WHERE email = '${email}'`
);
return result.rows[0];
}
// BAD: template literal — also vulnerable
async function searchProducts(name: string) {
const result = await pool.query(
`SELECT * FROM products WHERE name LIKE '%${name}%'`
);
return result.rows;
}// GOOD: parameterized query — injection impossible
async function getUser(email: string) {
const result = await pool.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
return result.rows[0];
}
// GOOD: parameterized LIKE query
async function searchProducts(name: string) {
const result = await pool.query(
"SELECT * FROM products WHERE name LIKE $1",
[`%${name}%`]
);
return result.rows;
}
// GOOD: using an ORM (Supabase, Prisma, Drizzle) — automatically parameterized
const { data } = await supabase
.from("users")
.select("*")
.eq("email", email);Search for string interpolation in SQL queries:
grep -n "query.*\$\{\|query.*\+\s" --include="*.ts" --include="*.tsx" -r src/Look for template literals or string concatenation inside .query(), .execute(), or .raw() calls.
$1, $2 for PostgreSQL; ? for MySQL)