Validate, then parameterize
Validate every body with a schema; bind values, never build SQL.
OWASPowasp.org
OWASP Top 10
From security-best-practices: Step 2, against OWASP A03.
One input, two queries
Type an email or pick a payload.
String interpolationBad
`… WHERE email = '${email}'`Bound parameterGood
SELECT * FROM users WHERE email = ?
params: []
'… WHERE email = ?', [value.email]// Bad
db.query(`SELECT * FROM users WHERE email = '${email}'`);
// Good
const user = await db.query(
'SELECT * FROM users WHERE email = ?',
[value.email]
);
Validate the body, answer 400
Edit a field or pick a body.
No schemaBad
db.query(sql, [req.body.email, …])Joi schemaGood
res.status(400).json({ error: error.details[0].message })const userSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).pattern(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/).required(),
name: Joi.string().min(2).max(50).required()
});
app.post('/api/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
const user = await db.query('SELECT * FROM users WHERE email = ?', [value.email]);
});
Test the password pattern
Type a password or pick one.
- 8+ chars
- A–Z
- a–z
- 0–9
- @$!%*?&
As in the skillUnanchored
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/
[A-Za-z\d@$!%*?&]AnchoredStrict
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
[A-Za-z\d@$!%*?&]{8,}$password: Joi.string().min(8)
.pattern(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
.messages({ 'string.pattern.base': '"password" needs A-Z, a-z, 0-9 and one of @$!%*?&' })
.required()
Why it matters, pros and cons
Why it matters
A bound parameter travels apart from the SQL, so input is never parsed as code.
Pros
- Closes injection for every value, even valid ones.
- The 400 names the exact field.
- Fixed query text reuses the prepared plan.
Cons
- Table, column and sort names can't be bound; allow-list them.
- Joi's default pattern message echoes the password back.
- ORM raw-SQL escape hatches skip parameterization.