Examples
Three side-by-side examples of vulnerable code and the parameterised fix — in PHP, Node, and Python. Walking the diffs builds the muscle memory that makes the secure pattern feel like the natural one to write.
Vulnerable vs parameterised in three languages
EXAMPLE
-- ============================================================
-- Example 1 — PHP (PDO)
-- A search-by-email handler concatenates input into the query.
-- ============================================================
-- VULNERABLE
<?php
$email = $_GET['email'] ?? '';
$sql = "SELECT id, name FROM users WHERE email = '$email'";
$rows = $pdo->query($sql)->fetchAll();
?>
-- FIXED — parameterised with named placeholders
<?php
$email = $_GET['email'] ?? '';
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$rows = $stmt->fetchAll();
?>
-- ============================================================
-- Example 2 — Node.js (pg)
-- An ORDER BY built from the client breaks parameterisation.
-- ============================================================
// VULNERABLE — both the WHERE and the ORDER BY are concatenated
const { Pool } = require('pg');
const pool = new Pool();
async function listProducts(category, sort) {
const sql = \`SELECT * FROM products WHERE category = '${category}'
ORDER BY ${sort} DESC\`;
return (await pool.query(sql)).rows;
}
// FIXED — parameterise the value; whitelist the column name
const SORT_COLS = { newest: 'created_at', price: 'price_cents', name: 'name' };
async function listProductsSafe(category, sort) {
const col = SORT_COLS[sort] ?? SORT_COLS.newest;
const { rows } = await pool.query(
\`SELECT * FROM products WHERE category = $1 ORDER BY ${col} DESC\`,
[category],
);
return rows;
}
-- ============================================================
-- Example 3 — Python (psycopg)
-- A LIKE-search builds the pattern by string concatenation.
-- ============================================================
# VULNERABLE
import psycopg
def search(needle: str, conn):
with conn.cursor() as cur:
cur.execute("SELECT id, title FROM articles WHERE title LIKE '%" + needle + "%'")
return cur.fetchall()
# FIXED — bind the pattern as a parameter; escape SQL wildcards in input
def search_safe(needle: str, conn):
safe = needle.replace('\\\\', '\\\\\\\\').replace('%', '\\\\%').replace('_', '\\\\_')
with conn.cursor() as cur:
cur.execute(
"SELECT id, title FROM articles WHERE title LIKE %s ESCAPE '\\\\'",
(f'%{safe}%',),
)
return cur.fetchall()
-- ============================================================
-- Bonus — ORM pitfall: raw escape hatches
-- The framework is safe; the raw escape hatch is where bugs live.
-- ============================================================
# Eloquent (Laravel) — UNSAFE
User::whereRaw("email = '$email'")->get();
# Eloquent — SAFE
User::where('email', $email)->get();
User::whereRaw('email = ?', [$email])->get();
# Sequelize — UNSAFE
sequelize.query("SELECT * FROM users WHERE id = " + req.params.id);
# Sequelize — SAFE
sequelize.query('SELECT * FROM users WHERE id = :id', {
replacements: { id: req.params.id }, type: QueryTypes.SELECT
});
Why it matters
Treat every raw-SQL helper your ORM exposes as a code-review trigger. The ORM defaults are safe; the bug almost always lives in the moment someone reached past the ORM to "just write the SQL". Adding a comment with rationale to each such call (`// raw because: ...`) keeps reviewers focused.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
-- Migrate one query at a time from concatenation to parameter binding. -- Add tests that POST a quote ' and assert a 4xx, not a 500.Try it Yourself »
Discussion
Loading…