FlawOpen

flawopen.com/sql-injection/Javascript

● CWE-89 · Critique
Recherche · FlawOpen

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]

💡 Explication en Langage Simple (ELI5)

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ésactive multipleStatements par défaut.[3], [6], [9]
pg query(text, values)
node-postgres envoie values séparément de text : 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
$queryRaw est 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]
Knex whereRaw / orderBy
whereRaw('email = ?', [email]) lie les valeurs, mais whereRaw et orderByRaw avec des chaînes interpolées sont du SQL brut. orderBy(column) cite la colonne comme identifiant ; ne lui passez que des noms issus d'une liste autorisée.[5], [6]

Correspondances de référentiels

Déroulement de l'Attaque Étape par Étape

Étape 1

La valeur de la requête atteint la couche de données

Une route Express lit req.query.id et le transmet à getUser(). Les paramètres de query sont des chaînes ou des tableaux, jamais des nombres, et rien ne les valide.[8], [6]

Étape 2

Un template literal construit le SQL

` ... WHERE id = ${userId} est évalué en premier. Avec la valeur de test inoffensive 0 OR 1=1, la chaîne devient WHERE id = 0 OR 1=1` avant que mysql2 ne la voie.[7], [1], [6]

Étape 3

Le pilote ne reçoit aucune valeur

pool.query(sql) a un tableau de valeurs vide : rien à échapper ni à lier. MySQL analyse OR 1=1 comme une partie de la clause WHERE.[3], [2], [9], [6]

Étape 4

La route renvoie la table

La condition est vraie pour chaque ligne. La route censée renvoyer un utilisateur les renvoie tous, et il en va de même via $queryRawUnsafe ou un orderByRaw alimenté par la query string.[1], [6]

Code Source : Vulnérable vs Sécurisé

IMPLÉMENTATION VULNÉRABLE
// 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);
}
PATCH SÉCURISÉ ET ROBUSTE
// 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

Sources