Applications and operations
Parameterize MySQL queries
Use placeholders for values so input cannot change the structure of a SQL statement.
Never build SQL by gluing user input into the string. This looks harmless:
// vulnerable — do not do this
const [rows] = await pool.query(
`SELECT id, title FROM notes WHERE title = '${searchTerm}'`
)
Now imagine searchTerm arrives from a form as ' OR '1'='1. The final statement becomes WHERE title = '' OR '1'='1', which is true for every row. The input stopped being data and became part of the query structure. That is SQL injection, and variants of it read other users’ rows, bypass logins, and delete tables.
With mysql2, pass values separately:
const [rows] = await pool.execute(
'SELECT id, title FROM notes WHERE id = ?',
[noteId]
)
Each ? is a placeholder. The statement text and the values travel to MySQL separately, and the server treats every value as data, never as SQL. The injection attempt above would just search for a note literally titled ' OR '1'='1 and find nothing.
Multiple values work the same way — the array fills the placeholders in order:
const [rows] = await pool.execute(
'SELECT id, title FROM notes WHERE title = ? AND estimated_hours < ?',
['Plan the week', 3]
)
pool.execute() uses real prepared statements, so the query structure is fixed on the server before any value arrives.
Verify the protection the same way you verify a grant: attack yourself. Send ' OR '1'='1 through your own search feature. A parameterized query returns an empty result; a vulnerable one returns everything. Two minutes, definitive answer.
One boundary to know: placeholders represent values, not table or column names. ORDER BY ? does not do what you hope — the driver would quote the column name as a string. When identifiers must vary, choose them from a fixed allowlist in your code:
const sortable = { date: 'created_at', title: 'title' }
const column = sortable[req.query.sort] ?? 'created_at'
Input selects an option you wrote; it never becomes SQL text itself.
Lesson completed