FlawOpen

flawopen.com/sql-injection/Javascript

● CWE-89 · Kritis
Riset Keamanan · FlawOpen

SQL Injection di JavaScript (mysql2, pg, Prisma, Knex)

Template literal atau + di dalam pool.query() membuat parameter request bisa menulis ulang SQL Anda dan mengembalikan setiap baris (CWE-89). Pemanggilan yang aman di mysql2, node-postgres, Prisma, dan Knex, alasan ${} aman di satu metode Prisma tetapi berbahaya di metode sebelahnya, dan cara mengurutkan tanpa orderByRaw.[1], [2], [3], [4], [5], [6]

💡 Penjelasan Sederhana (ELI5)

Bayangkan pusat sortir paket: rute dicetak dan dikunci sebelum paket tiba. Nama pada label masuk ke kolom yang disediakan; nama itu tidak bisa menambahkan pemberhentian ke rute. Parameter SQL bekerja serupa: mysql2 execute(sql, [userId]) dan pg query(sql, [status]) mengikat nilai, sedangkan $queryRaw bertag milik Prisma mengikat setiap posisi ${}. Jika SQL dirangkai lebih dulu dengan template literal atau +, atau memakai $queryRawUnsafe, teks pelanggan tertulis langsung pada rute. Knex orderByRaw memiliki risiko yang sama untuk instruksi pengurutan; petakan kunci permintaan ke kolom tetap lalu panggil orderBy.[1], [2], [3], [4], [5], [6]

Konsep Kunci & Istilah

Template literals in query()
String backtick yang berisi ${userId} dievaluasi JavaScript sebelum driver dipanggil, sehingga driver menerima SQL jadi dengan nilai yang sudah ada di dalamnya. Sintaksnya mirip placeholder, tetapi tidak mem-bind apa pun.[7], [1], [6]
mysql2 execute() vs query()
execute(sql, values) memakai prepared statement di sisi server. query(sql, values) meng-escape nilai di sisi klien lalu mengisinya ke penanda ?. Keduanya aman selama nilai ada di array; tidak ada yang membantu jika string SQL disusun dengan + atau template literal. mysql2 menonaktifkan multipleStatements secara bawaan.[3], [6], [9]
pg query(text, values)
node-postgres mengirim values terpisah dari text, jadi $1 diisi oleh PostgreSQL. Tanpa nilai, pg memakai protokol kueri sederhana, yang juga menerima beberapa perintah yang dipisahkan titik koma dalam satu panggilan.[2], [6]
Prisma $queryRaw vs $queryRawUnsafe
$queryRaw adalah tagged template: Prisma mengubah setiap ${} menjadi parameter bind. $queryRawUnsafe(string) menerima string jadi, sehingga ${} yang sama di dalam template literal biasa ditempelkan ke SQL. Namanya hanya beda satu kata, tetapi risikonya berkebalikan.[4], [6]
Knex whereRaw / orderBy
whereRaw('email = ?', [email]) mem-bind nilai, tetapi whereRaw dan orderByRaw dengan string terinterpolasi adalah SQL mentah. orderBy(column) mengutip kolom sebagai identifier; berikan hanya nama dari allowlist.[5], [6]

Pemetaan kerangka kerja

Alur Serangan Langkah demi Langkah

Langkah 1

Nilai request sampai ke lapisan data

Sebuah route Express membaca req.query.id dan meneruskannya ke getUser(). Parameter query berupa string atau array, tidak pernah angka, dan tidak ada yang memvalidasinya.[8], [6]

Langkah 2

Template literal menyusun SQL

` ... WHERE id = ${userId} dievaluasi lebih dulu. Dengan nilai uji yang tidak berbahaya 0 OR 1=1, string menjadi WHERE id = 0 OR 1=1` sebelum mysql2 melihatnya.[7], [1], [6]

Langkah 3

Driver tidak menerima nilai

pool.query(sql) punya array nilai kosong, jadi tidak ada yang perlu di-escape atau di-bind. MySQL mengurai OR 1=1 sebagai bagian klausa WHERE.[3], [2], [9], [6]

Langkah 4

Route mengembalikan tabel

Kondisinya benar untuk setiap baris. Route yang seharusnya mengembalikan satu pengguna mengembalikan semuanya, dan hal yang sama terjadi lewat $queryRawUnsafe atau orderByRaw yang diisi dari query string.[1], [6]

Kode Sumber: Rentan vs Aman

IMPLEMENTASI RENTAN
// users.js: setiap kueri di bawah menyusun teks SQL dari data request

// mysql2: template literal dievaluasi sebelum driver dipanggil
export async function getUser(pool, userId) {
  const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
  return rows[0];
}

// pg: penggabungan string, dan tidak ada array nilai untuk di-bind
export async function findOrders(pg, status) {
  const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
  return rows;
}

// Prisma: varian Unsafe menerima string jadi
export function findByEmail(prisma, email) {
  return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}

// Knex: orderByRaw menyisipkan nilai request sebagai SQL mentah
export function listUsers(knex, sortColumn) {
  return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
PERBAIKAN AMAN & KUAT
// users.js: nilai masuk ke array nilai, kolom berasal dari allowlist
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);

// mysql2: prepared statement di server, nilai di-bind ke ?
export async function getUser(pool, userId) {
  const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
  return rows[0];
}

// pg: PostgreSQL mengisi $1 dari array nilai
export async function findOrders(pg, status) {
  const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
  return rows;
}

// Prisma: tagged template, setiap ${} menjadi parameter bind
export function findByEmail(prisma, email) {
  return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}

// Knex: orderBy mengutip kolom dari allowlist sebagai identifier
export function listUsers(knex, sortKey) {
  const column = SORTABLE.get(sortKey) ?? 'id';
  return knex('users').select('id', 'email').orderBy(column);
}

Daftar Periksa Penguatan Sistem Rekayasa

Sumber