OWASP Top 10 and Injections: Parameterized Queries

First lesson of the Authentication and Web Security (OWASP) course, free to read: about 15 minutes, 3 exercises and 3 quiz questions. This is the catalogue's base lesson; in the student space, it is rewritten for your profile.

Objectives

  • Place the main risk families of the OWASP Top 10 2025
  • Explain the mechanism of a SQL injection
  • Write parameterized queries and allowlists for identifiers

OWASP Top 10SQL injectionParameterized queries

OWASP (Open Worldwide Application Security Project) is a foundation that publishes free resources on application security. Its best-known document, the Top 10, ranks the most critical risks of web applications. The 2025 edition includes:

Rank Category
A01 Broken Access Control (now includes SSRF)
A02 Security Misconfiguration
A03 Software Supply Chain Failures
A04 Cryptographic Failures
A05 Injection
A06 Insecure Design
A07 Authentication Failures
A08 Software or Data Integrity Failures
A09 Logging and Alerting Failures
A10 Mishandling of Exceptional Conditions

It is not a complete standard: it is an awareness list. For an audit, you rely instead on OWASP’s ASVS (Application Security Verification Standard). In this course, we will concretely address several of these categories, starting with injection.

The mechanism of an injection

An injection occurs when data supplied by the user is interpreted as code. A classic example in Node.js:

// DANGEROUS: the input is pasted into the SQL text
const email = req.body.email; // "x' OR '1'='1"
const sql = "SELECT * FROM users WHERE email = '" + email + "'";
// => SELECT * FROM users WHERE email = 'x' OR '1'='1'
// The condition is always true: the whole table is returned.

The attacker closed the string with an apostrophe and then added their own logic. Depending on the database, they can also read other tables (UNION SELECT) or modify data.

The solution: the parameterized query

With a parameterized (or prepared) query, you send the SQL server the structure on one side and the values on the other. The value can never become SQL, whatever it contains.

// With the PostgreSQL "pg" driver: placeholders $1, $2...
const { rows } = await pool.query(
  "SELECT id, email FROM users WHERE email = $1",
  [email] // value passed separately
);

// With the native node:sqlite module (Node 22.5+) or MySQL: "?" placeholders
const stmt = db.prepare("SELECT id, email FROM users WHERE email = ?");
const user = stmt.get(email);

ORMs (Prisma, Sequelize, Drizzle…) parameterize automatically, unless you use their raw-query functions with concatenation.

The identifier trap

Placeholders only work for values. A column name or a sort direction (ORDER BY price DESC) cannot be parameterized. You then use an allowlist:

const COLONNES_TRI = new Set(["price", "name", "created_at"]);
function clauseTri(col, sens) {
  if (!COLONNES_TRI.has(col)) throw new Error("Invalid sort column");
  const dir = sens === "desc" ? "DESC" : "ASC"; // only two possible values
  return `ORDER BY ${col} ${dir}`;
}

Key takeaways

  • Never build SQL (or a shell command) by concatenating external data.
  • Replace “escape the apostrophes” with parameterized queries.
  • Validate with an allowlist anything that cannot be parameterized.
  • The same principle applies to other injections: child_process.execFile with an array of arguments rather than exec with a string.

Exercises

Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.

  1. CodeDifficulty 1JavaScript in the browser2 hints

    Build a parameterized query

    Write findUserByEmail(email) that never concatenates the email into the SQL and returns a { text, values } object (the pg driver’s format):

    • text is exactly "SELECT id, email FROM users WHERE email = $1";
    • values is an array containing the email.

    If email is not a string, throw a TypeError.

    function findUserByEmail(email) {
      // À compléter
    }
  2. CodeDifficulty 2JavaScript in the browser2 hints

    Allowlist for ORDER BY

    Write buildOrderBy(column, direction):

    • allowed columns: name, price, created_at; otherwise throw an Error;
    • direction is "desc" (case-insensitive) for DESC, any other value gives ASC;
    • returns for example "ORDER BY price DESC".
    function buildOrderBy(column, direction) {
      // À compléter
    }
  3. Multiple choiceDifficulty 11 hint

    Identify the right defense

    Which practice effectively protects a SQL query against injection?

    • Replacing apostrophes with two apostrophes
    • Using a parameterized query with placeholders
    • Limiting the field length to 50 characters
    • Encoding the input in Base64 before concatenating it

End-of-lesson quiz

  1. What does injection mean in the OWASP Top 10?

    • A server that injects ads
    • External data interpreted as code by an interpreter
    • A malicious npm package
    • A password that is too short
  2. Can you pass a column name as a $1 parameter?

    • Yes, always
    • No: placeholders are only for values, you need an allowlist
    • Yes, but only with MySQL
  3. Which Node function limits the risk of command injection?

    • child_process.exec with a string
    • child_process.execFile with an array of arguments
    • eval

The rest of the course

  1. XSS: Escaping HTML and Setting a CSP15 min
  2. Storing Passwords: Hashing with scrypt18 min
  3. Sessions, Signed Tokens, and Secure Cookies18 min
  4. Access Control, Security Headers, and Error Handling18 min

Authentication and Web Security (OWASP) · Advanced · 3 h 24 min