Injection and output

Use parameterized database queries

Keep SQL structure separate from values so user input cannot become executable database syntax.

SQL injection appears when data is concatenated into a query string. Once the value and the query share one channel, a crafted value can rewrite the query’s logic. Parameters keep the value in the value channel where it can only ever be data.

Here is the pattern that fails. The search term is glued directly into the SQL text.

// Vulnerable: input becomes part of the query structure
const rows = await db.query(
  `SELECT id, title FROM notes WHERE owner_id = ${ownerId} AND title = '${term}'`
)

A term of ' OR '1'='1 closes the string early and changes the condition. The database now returns rows the user was never allowed to see.

Bind values, allowlist identifiers

The safe version sends the query text and the values separately. The driver never treats a value as syntax.

SELECT id, title
FROM notes
WHERE owner_id = ? AND id = ?
const rows = await db.query(
  'SELECT id, title FROM notes WHERE owner_id = ? AND id = ?',
  [ownerId, noteId],
)

Parameters cover values, not identifiers. A column name for sorting cannot be a bound parameter, so map it through a fixed lookup.

const sortColumns = { title: 'title', created: 'created_at' }
const orderBy = sortColumns[req.query.sort] ?? 'created_at' // reject anything else

A search query concatenates owner_id and the search term into SQL. A crafted quote changes the condition and returns another user’s notes.

Parameters protect values, not dynamic table names or sort columns. Keep identifiers in a fixed mapping instead of passing them through the value API.

Replace one concatenated query with parameters and capture its result for a title containing a quote. Test SQL control characters and an unsupported sort field, then verify no extra rows or schema changes appear.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →