flawopen.com/sql-injection/Javascript
Injection SQL en JavaScript (mysql2, pg, Prisma, Knex)
Un template literal ou un + dans pool.query() laisse un paramètre de requête réécrire votre SQL et renvoyer toutes les lignes (CWE-89). L'appel sûr avec mysql2, node-postgres, Prisma et Knex, pourquoi ${} est sûr dans une méthode Prisma et dangereux dans la suivante, et comment trier sans orderByRaw.[1], [2], [3], [4], [5], [6]
Imaginez un centre de tri : l'itinéraire est imprimé et verrouillé avant l'arrivée des colis. Le nom sur l'étiquette occupe un champ prévu ; il ne peut pas ajouter une étape à la tournée. Les paramètres SQL fonctionnent pareil : mysql2 execute(sql, [userId]) et pg query(sql, [status]) lient des valeurs, tandis que le $queryRaw balisé de Prisma lie chaque emplacement ${}. Si vous construisez d'abord le SQL avec un template literal ou +, ou utilisez $queryRawUnsafe, le texte du client est inscrit dans l'itinéraire lui-même. Knex orderByRaw présente le même risque pour le tri ; associez la clé demandée à une colonne fixe et appelez orderBy.[1], [2], [3], [4], [5], [6]
Concepts Clés et Termes
Template literals in query()- Une chaîne entre backticks contenant
${userId}est évaluée par JavaScript avant l'appel au pilote : celui-ci reçoit un SQL terminé, valeur déjà incluse. La syntaxe ressemble à un marqueur mais ne lie rien.[7], [1], [6] mysql2 execute() vs query()execute(sql, values)utilise une instruction préparée côté serveur.query(sql, values)échappe les valeurs côté client et les substitue aux marqueurs?. Les deux sont sûrs si les valeurs sont dans le tableau ; aucun n'aide si la chaîne SQL a été construite avec+ou un template literal. mysql2 désactivemultipleStatementspar défaut.[3], [6], [9]pg query(text, values)- node-postgres envoie
valuesséparément detext: c'est PostgreSQL qui remplit$1. Sans valeurs, pg utilise le protocole de requête simple, qui accepte aussi plusieurs instructions séparées par des points-virgules en un appel.[2], [6] Prisma $queryRaw vs $queryRawUnsafe$queryRawest un tagged template : Prisma transforme chaque${}en paramètre lié.$queryRawUnsafe(string)reçoit une chaîne toute faite, donc le même${}dans un template literal ordinaire est collé dans le SQL. Un seul mot sépare les deux noms, et le risque est inverse.[4], [6]
Correspondances de référentiels
- Référence CWE pour la faiblesse décrite dans ce guide : CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Déroulement de l'Attaque Étape par Étape
Code Source : Vulnérable vs Sécurisé
// users.js : chaque requête ci-dessous construit du SQL à partir de données HTTP
// mysql2 : le template literal est évalué avant l'appel au pilote
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg : concaténation, et aucun tableau de valeurs à lier
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma : la variante Unsafe reçoit une chaîne toute faite
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex : orderByRaw insère la valeur de la requête en SQL brut
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
// users.js : les valeurs vont dans le tableau de valeurs, les colonnes viennent d'une liste autorisée
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2 : instruction préparée côté serveur, valeur liée au ?
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg : PostgreSQL remplit $1 depuis le tableau de valeurs
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma : tagged template, chaque ${} devient un paramètre lié
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex : orderBy cite la colonne autorisée comme identifiant
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
Liste de Contrôle de Sécurité pour l'Ingénierie
- ✓Interdisez
$queryRawUnsafe,$executeRawUnsafe,whereRaw/orderByRawavec interpolation, ainsi que les template literals ou+dans le premier argument dequery()/execute(), via une règle ESLintno-restricted-syntaxou Semgrep en CI.[1], [3], [2], [4], [5], [6] - ✓Passez chaque valeur dans le tableau de valeurs du pilote (
?pour mysql2,$1pour pg) ou comme${}dans le$queryRawbalisé de Prisma.[3], [2], [4], [6] - ✓Validez l'entrée avec un schéma (zod, Joi) dans la route : les identifiants deviennent des nombres, les clés inconnues sont refusées et aucun tableau n'est accepté là où une chaîne est attendue.[8], [6]
- ✓Faites passer les clés de tri par une liste autorisée
Mapet donnez le résultat àorderBy()de Knex ou àorderByde Prisma ; ne passez jamaisreq.queryà un ORDER BY brut.[5], [4], [6] - ✓Laissez
multipleStatementsde mysql2 désactivé et connectez-vous avec un utilisateur de base sans droits DDL.[3], [9], [6]
Sources
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- node-postgres — Parameterized query
- mysql2 — Using prepared statements
- Prisma — Raw queries and SQL injection
- Knex.js — Raw parameter binding
- OWASP SQL Injection Prevention Cheat Sheet
- MDN — Template literals (JavaScript)
- Express 4 API — req.query
- MySQL C API — Multiple statement execution support