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.execFilewith an array of arguments rather thanexecwith 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.
Build a parameterized query
Write
findUserByEmail(email)that never concatenates the email into the SQL and returns a{ text, values }object (thepgdriver’s format):textis exactly"SELECT id, email FROM users WHERE email = $1";valuesis an array containing the email.
If
emailis not a string, throw aTypeError.function findUserByEmail(email) { // À compléter }Allowlist for ORDER BY
Write
buildOrderBy(column, direction):- allowed columns:
name,price,created_at; otherwise throw anError; directionis"desc"(case-insensitive) forDESC, any other value givesASC;- returns for example
"ORDER BY price DESC".
function buildOrderBy(column, direction) { // À compléter }- allowed columns:
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
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
Can you pass a column name as a
$1parameter?- Yes, always
- No: placeholders are only for values, you need an allowlist
- Yes, but only with MySQL
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
- XSS: Escaping HTML and Setting a CSP15 min
- Storing Passwords: Hashing with scrypt18 min
- Sessions, Signed Tokens, and Secure Cookies18 min
- Access Control, Security Headers, and Error Handling18 min