Metode query standar ORM seperti findMany, findUnique, create, update, dan delete milik Prisma menggunakan parameterized query secara internal, sehingga input pengguna tidak pernah digabungkan ke dalam string SQL. Namun, celah raw query — $queryRaw dan $executeRaw Prisma, QueryBuilder TypeORM dengan penggabungan string, serta raw query Sequelize — membawa kembali kerentanan ini jika digunakan secara tidak benar. Penulis mengaudit setiap penggunaan raw query di seluruh codebase pada setiap proyek ERP.
Gunakan sintaks tag template literal Prisma (misalnya prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`) daripada penggabungan string. Sintaks ini secara otomatis memparameterkan semua nilai yang diinterpolasi. Untuk nama kolom atau tabel dinamis yang tidak dapat diparameterkan, validasi input secara ketat terhadap allowlist nilai yang diizinkan secara eksplisit — jangan pernah gunakan blocklist untuk kode kritis keamanan.
Penulis menjalankan sqlmap terhadap setiap endpoint API staging yang menerima input pengguna sebelum setiap rilis, menggunakan flag level 3 dan risk 2 untuk endpoint GET, serta --data dengan --method=POST untuk endpoint POST. Proses ini memakan waktu 5–10 menit per endpoint dan telah menangkap kerentanan injeksi dalam kode raw query yang lolos dari code review. Jika sqlmap menemukan sesuatu, endpoint tersebut tidak akan di-deploy.
Injeksi second-order terjadi ketika input pengguna disimpan dengan aman melalui parameterized query, lalu diambil kembali dan digunakan secara tidak aman dalam string raw query. Misalnya, username berisi admin'-- tersimpan dengan aman, tetapi kemudian menjadi injeksi ketika fitur panel admin membangun query laporan mentah menggunakan nilai yang tersimpan itu. Karena injeksi terjadi pada penggunaan kedua, bukan yang pertama, ini melewati pengujian yang hanya memeriksa input di titik masuk. Pertahanannya adalah memparameterkan semua query dan memperlakukan semua data dari database sebagai tidak tepercaya.
Daftar periksa deployment penulis mencakup: pengguna database hanya diberikan izin SELECT, INSERT, UPDATE, dan DELETE (tanpa izin DDL); pengguna database terpisah per layanan, bukan satu superuser untuk semua; connection string disimpan dalam variabel lingkungan; pg_hba.conf membatasi akses ke IP server aplikasi saja; pgaudit diaktifkan pada tabel keuangan untuk compliance logging; batas waktu query diatur ke 30 detik; dan batas connection pool untuk mencegah denial-of-service akibat kelelahan koneksi.
SQL injection pertama kali didokumentasikan pada 1998 dan berada di peringkat #3 dalam OWASP Top 10 2021. Dua puluh tujuh tahun kemudian, ini tetap menjadi salah satu kerentanan yang paling banyak dieksploitasi. Alasannya bukan ketidaktahuan — kebanyakan developer tahu apa itu SQL injection. Alasannya adalah rasa puas: 'Saya menggunakan ORM, jadi saya aman.' Itu sebagian benar dan berbahaya secara keliru. ORM seperti Prisma, TypeORM, dan Sequelize mencegah injeksi dalam metode query standar, tetapi semuanya menyediakan celah raw query yang membawa kerentanan kembali jika digunakan secara tidak benar.
Metode query Prisma (findMany, findUnique, create, update, delete) menggunakan parameterized query secara internal — input pengguna masuk ke slot parameter, tidak pernah ke string SQL itu sendiri. Di mana Anda kehilangan perlindungan: $queryRaw dan $executeRaw Prisma dengan tag template literal, QueryBuilder TypeORM dengan string yang digabungkan, raw query Sequelize, dan setiap kali Anda membangun string klausa WHERE dari input pengguna dan meneruskannya ke fungsi raw query.
Pola injeksi yang saya temukan dalam code review: (1) Interpolasi string di $queryRaw — `prisma.$queryRaw('SELECT * FROM users WHERE email = ' + email)` dapat diinjeksi. Versi aman menggunakan tag template literal. (2) Dynamic ORDER BY — ORM biasanya tidak dapat memparameterkan nama kolom. `ORDER BY ${userInput}` dapat diinjeksi. Validasi nama kolom terhadap whitelist kolom sort yang diizinkan. (3) Nama tabel dinamis — jangan pernah membiarkan input pengguna menentukan nama tabel.
// DANGEROUS — string concatenation in raw query
const email = req.body.email // attacker sends: ' OR '1'='1
await prisma.$queryRaw('SELECT * FROM users WHERE email = ' + email)
// → dumps entire users table
// SAFE — tagged template literal (auto-parameterized by Prisma)
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`
// DANGEROUS — dynamic ORDER BY with user input
const sortColumn = req.query.sort // attacker sends: id; DROP TABLE users--
await prisma.$queryRaw`SELECT * FROM invoices ORDER BY ${sortColumn}`
// SAFE — allowlist validation for column names
const ALLOWED_SORT_COLUMNS = ['createdAt', 'amount', 'invoiceNumber', 'status'] as const
type SortColumn = typeof ALLOWED_SORT_COLUMNS[number]
function validateSortColumn(col: string): SortColumn {
if (!ALLOWED_SORT_COLUMNS.includes(col as SortColumn)) {
throw new BadRequestException(`Invalid sort column: ${col}`)
}
return col as SortColumn
}
const safeSort = validateSortColumn(req.query.sort as string)
// Use Prisma's safe orderBy instead of raw SQL
const invoices = await prisma.invoice.findMany({
orderBy: { [safeSort]: req.query.order === 'desc' ? 'desc' : 'asc' },
where: { userId: req.user.id }
})
// Testing with sqlmap
// sqlmap -u 'https://staging.api.com/invoices?sort=createdAt' \
// --cookie='Authorization=Bearer <token>' \
// --level=3 --risk=2 --batchDari pengalaman saya mengamankan sistem ERP: tambahkan aturan ESLint kustom ke pipeline CI Anda yang menandai penggunaan $queryRaw atau $executeRaw dengan pola non-template-literal. Aturan tersebut menangkap antipattern injeksi paling umum sebelum mencapai code review. Saya juga menjalankan sqlmap terhadap lingkungan staging setelah perubahan skema apa pun.
Ketika Anda benar-benar membutuhkan SQL mentah (fungsi window yang kompleks, fitur khusus database, query kritis performa), gunakan sintaks tag template literal Prisma yang secara otomatis memparameterkan semua nilai yang diinterpolasi. Untuk nama kolom dan tabel dinamis yang tidak dapat diparameterkan, implementasikan validasi allowlist yang ketat — jangan pernah gunakan validasi blocklist untuk kode kritis keamanan.
Saya menjalankan sqlmap terhadap setiap endpoint API staging yang menerima input pengguna sebelum rilis. Pemindaian dasar: `sqlmap -u 'https://staging.example.com/api/users?id=1' --cookie='auth=<token>' --level=3 --risk=2`. Untuk endpoint POST: `sqlmap -u 'https://staging.example.com/api/search' --data='query=test' --method=POST`. sqlmap secara otomatis menguji lusinan teknik dan payload injeksi.
Dalam sistem ERP klien yang saya warisi untuk pemeliharaan, saya menemukan endpoint pencarian yang membangun klausa WHERE dari parameter query URL: developer sebelumnya telah menggabungkan input pengguna ke dalam TypeORM QueryBuilder. Endpoint telah aktif selama 6 bulan. Saya mengujinya dengan injeksi dasar `' OR '1'='1` dan API mengembalikan semua catatan dalam tabel, melewati filter tenant. Tidak ada bukti telah dieksploitasi, tetapi paparannya nyata.
Injeksi first-order (input langsung ke query) adalah apa yang diuji kebanyakan developer. Injeksi second-order (input disimpan dengan aman, kemudian diambil dan digunakan tidak aman nanti) kurang dipahami dengan baik. Contoh: pengguna mendaftar dengan username `admin'--`. Pendaftaran menyimpan ini dengan aman melalui parameterized query. Kemudian, fitur panel admin mengambil username dan menggunakannya dalam string raw query untuk laporan. Injeksi menyala pada penggunaan kedua, bukan yang pertama.
Pencegahan SQL injection adalah satu lapisan keamanan database. Juga implementasikan: prinsip hak istimewa terkecil untuk pengguna database (pengguna API tidak boleh memiliki izin DROP TABLE atau CREATE USER), pengguna database terpisah per layanan, connection pooling dengan batas untuk mencegah DoS kelelahan koneksi, batas waktu query (matikan query yang berjalan lebih dari 30 detik), dan audit logging untuk tabel sensitif.
-- PostgreSQL security configuration
-- 1. Dedicated app user with minimal privileges
CREATE USER app_user WITH PASSWORD 'strong_password';
GRANT CONNECT ON DATABASE erp_db TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
-- Note: no DDL permissions (no CREATE, DROP, ALTER)
-- 2. Enable audit logging for sensitive tables
CREATE EXTENSION IF NOT EXISTS pgaudit;
-- postgresql.conf:
-- pgaudit.log = 'write, ddl'
-- pgaudit.log_relation = on
-- 3. Row-level security for multi-tenant data
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::UUID);
-- 4. Set statement timeout
ALTER ROLE app_user SET statement_timeout = '30s';
ALTER ROLE app_user SET idle_in_transaction_session_timeout = '60s';Yang saya verifikasi di setiap deployment: semua input pengguna melalui parameterized query ORM atau divalidasi terhadap allowlist sebelum penggunaan raw query, pengguna database memiliki SELECT/INSERT/UPDATE/DELETE saja (tidak ada DDL), connection string dalam variabel lingkungan (tidak dalam kode), pg_hba.conf membatasi akses database ke IP server aplikasi saja, pgaudit diaktifkan pada tabel keuangan, batas waktu query diatur ke 30 detik. Menjalankan sqlmap terhadap staging sebelum setiap rilis adalah langkah verifikasi final.
Sumber & Bacaan Lanjutan