flawopen.com/sql-injection/Javascript
SQL-Injection in JavaScript verhindern (mysql2, pg, Prisma, Knex)
Ein Template-Literal oder + in pool.query() lässt einen Request-Parameter Ihr SQL umschreiben und jede Zeile zurückgeben (CWE-89). Der sichere Aufruf in mysql2, node-postgres, Prisma und Knex, warum ${} in einer Prisma-Methode sicher und in der nächsten gefährlich ist und wie man ohne orderByRaw sortiert.[1], [2], [3], [4], [5], [6]
Stell dir ein Paketzentrum vor: Der Fahrplan wird gedruckt und abgeschlossen, bevor Pakete eintreffen. Der Name auf einem Etikett gehört in ein festes Feld; er kann keinen zusätzlichen Halt in den Fahrplan schreiben. SQL-Parameter funktionieren genauso: mysql2 execute(sql, [userId]) und pg query(sql, [status]) binden Werte, und Prismas getaggtes $queryRaw bindet jede ${}-Lücke. Baust du SQL vorher mit einem Template Literal oder + zusammen oder verwendest $queryRawUnsafe, steht der Kundentext im Fahrplan selbst. Knex orderByRaw birgt dasselbe Risiko für Sortieranweisungen; ordne den angefragten Schlüssel einer festen Spalte zu und rufe orderBy auf.[1], [2], [3], [4], [5], [6]
Kernkonzepte & Begriffe
Template literals in query()- Ein Backtick-String mit
${userId}wird von JavaScript ausgewertet, bevor der Treiber aufgerufen wird. Der Treiber erhält also fertiges SQL mit dem Wert darin. Die Syntax sieht aus wie ein Platzhalter, bindet aber nichts.[7], [1], [6] mysql2 execute() vs query()execute(sql, values)nutzt ein serverseitiges Prepared Statement.query(sql, values)maskiert die Werte im Client und setzt sie in die?-Marker ein. Beides ist sicher, solange die Werte im Array stehen; keins hilft, wenn der SQL-String mit+oder einem Template-Literal gebaut wurde. mysql2 schaltetmultipleStatementsstandardmäßig ab.[3], [6], [9]pg query(text, values)- node-postgres schickt
valuesgetrennt vontext,$1füllt also PostgreSQL. Ohne Werte nutzt pg das einfache Query-Protokoll, das auch mehrere durch Semikolon getrennte Anweisungen in einem Aufruf annimmt.[2], [6] Prisma $queryRaw vs $queryRawUnsafe$queryRawist ein Tagged Template: Prisma macht aus jedem${}einen gebundenen Parameter.$queryRawUnsafe(string)nimmt einen fertigen String, dasselbe${}in einem normalen Template-Literal landet also im SQL. Die Namen unterscheiden sich um ein Wort, das Risiko ist das Gegenteil.[4], [6]
Framework-Zuordnungen
- CWE-Referenz für die in diesem Leitfaden beschriebene Schwachstelle: CWE-89
- OWASP Top 10:2025 — A05:2025 Injection
Schritt-für-Schritt Angriffsablauf
Quellcode: Verwundbar vs. Sicher
// users.js: jede Abfrage unten baut SQL-Text aus Request-Daten
// mysql2: das Template-Literal wird ausgewertet, bevor der Treiber läuft
export async function getUser(pool, userId) {
const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
return rows[0];
}
// pg: Verkettung, und kein Werte-Array zum Binden
export async function findOrders(pg, status) {
const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
return rows;
}
// Prisma: die Unsafe-Variante nimmt einen fertigen String
export function findByEmail(prisma, email) {
return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}
// Knex: orderByRaw fügt den Request-Wert als rohes SQL ein
export function listUsers(knex, sortColumn) {
return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
// users.js: Werte stehen im Werte-Array, Spalten kommen aus einer Allowlist
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);
// mysql2: serverseitiges Prepared Statement, Wert an ? gebunden
export async function getUser(pool, userId) {
const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
return rows[0];
}
// pg: PostgreSQL füllt $1 aus dem Werte-Array
export async function findOrders(pg, status) {
const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
return rows;
}
// Prisma: Tagged Template, jedes ${} wird ein gebundener Parameter
export function findByEmail(prisma, email) {
return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}
// Knex: orderBy quotet die freigegebene Spalte als Bezeichner
export function listUsers(knex, sortKey) {
const column = SORTABLE.get(sortKey) ?? 'id';
return knex('users').select('id', 'email').orderBy(column);
}
Checkliste für Engineering & Systemsicherheit
- ✓
$queryRawUnsafe,$executeRawUnsafe,whereRaw/orderByRawmit Interpolation sowie Template-Literals oder+im ersten Argument vonquery()/execute()per ESLint-Regelno-restricted-syntaxoder Semgrep im CI verbieten.[1], [3], [2], [4], [5], [6] - ✓Jeden Wert im Werte-Array des Treibers übergeben (
?bei mysql2,$1bei pg) oder als${}im getaggten$queryRawvon Prisma.[3], [2], [4], [6] - ✓Eingaben in der Route mit einem Schema (zod, Joi) validieren: IDs werden zu Zahlen, unbekannte Schlüssel werden abgelehnt, Arrays werden dort, wo ein String erwartet wird, nicht akzeptiert.[8], [6]
- ✓Sortierschlüssel über eine
Map-Allowlist führen und das Ergebnis an KnexorderBy()oder PrismasorderBygeben; niereq.queryan ein rohes ORDER BY übergeben.[5], [4], [6] - ✓
multipleStatementsin mysql2 ausgeschaltet lassen und mit einem Datenbanknutzer ohne DDL-Rechte verbinden.[3], [9], [6]
Quellen
- 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