Validate, then parameterize

Validate every body with a schema; bind values, never build SQL.

OWASPowasp.org
OWASP Top 10
ReferenceRead docs ↗

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.

          1. 8+ chars
          2. A–Z
          3. a–z
          4. 0–9
          5. @$!%*?&
          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.