Databases in practice
Use parameterized queries
Keep SQL syntax separate from untrusted values so submitted data cannot become executable query text.
Never build a query by concatenating user input into the SQL string.
This is unsafe because submittedEmail becomes part of the SQL program:
const sql = `SELECT id, email FROM users WHERE email = '${submittedEmail}'`
const result = await db.query(sql)
If submittedEmail is ' OR 1=1 --, the database may return every user row.
Use a placeholder and pass the value separately:
const result = await db.query(
'SELECT id, email FROM users WHERE email = $1',
[submittedEmail]
)
The database receives the SQL structure and the value through separate channels. A value such as ' OR 1=1 -- remains a string to compare with email. It cannot add an OR condition to the query. The result should be empty unless that exact email exists.
Placeholder syntax depends on the driver. PostgreSQL drivers commonly use $1, while other drivers use ? or named placeholders. Use the parameter API documented by your driver rather than trying to escape input yourself.
Parameters represent values. They usually cannot stand for table names, column names, keywords, or sort directions. If a user can choose a sort order, map a small allow-listed value to SQL you control:
const orderBy = submittedSort === 'oldest'
? 'created_at ASC'
: 'created_at DESC'
Parameterized queries prevent SQL injection through values. They do not replace input validation or authorization. A safely encoded user ID is still dangerous if the signed-in user is not allowed to access it.
Log the query shape separately from parameter values in production. That helps debugging without leaking user input into log files.
Every major driver supports parameters. If your ORM generates raw concatenated SQL, switch to its parameterized query API.
Try this on your own: pass a quote-heavy string such as ' OR 1=1 -- through a parameterized lookup. Verify that it is treated as one email value and that no unrelated rows are returned.
Lesson completed