Membedah tiga serangan paling berbahaya di API: SQL injection, IDOR yang membocorkan data milik user lain, dan SSRF yang menjadikan server sebagai pelompat, lengkap dengan cara menemukan dan menambalnya

Episode 17 membangun pertahanan dasar. Sekarang kita membedah tiga serangan paling berbahaya yang menyerang logika API itu sendiri — bukan sekadar konfigurasi: SQL injection, IDOR, dan SSRF. Ketiganya termasuk kelas "broken access control" dan "injection" di OWASP Top 10, dan semuanya bisa ditemukan lewat review kode yang sistematis.
Mengapa harus paham tiga serangan ini secara mendalam? Karena semuanya bisa dicegah di level kode, dan semuanya rutin ditemukan di aplikasi produksi nyata — termasuk di perusahaan besar. Security review yang baik bukan menebak-nebak, melainkan mencari pola spesifik. Episode ini mengajarkan pola itu dan cara menambalnya.
Kita sudah menyentuhnya di episode 17 — sekarang kita bedah mengapa ia begitu merusak dan bentuk-bentuknya.
Terkadang output tidak tampil di response, tapi bisa disimpulkan dari perbedaan perilaku. Contoh: payload ' AND 1=1-- selalu berhasil, ' AND 1=2-- selalu gagal. Penyerang bisa menebak karakter demi karakter isi database — disebut blind boolean-based injection:
/users?q=' OR (SELECT substring(password,1,1) FROM users LIMIT 1)='a'--
/users?q=' OR (SELECT substring(password,1,1) FROM users LIMIT 1)='b'--Setiap percobaan memberi jawaban benar/salah — dan dengan 62 karakter x panjang password, seluruh hash bisa ditebak otomatis.
const { rows } = await db.query(
`SELECT * FROM users WHERE email = $1 AND password_hash = $2`,
[email, hash],
)Whitelist untuk struktur — untuk ORDER BY/LIMIT yang tidak bisa parameterized, jangan masukkan input mentah:
const ALLOWED_SORT = new Set(["price", "created_at", "name"])
function buildOrderBy(input: string) {
if (!ALLOWED_SORT.has(input)) return "created_at"
return input
}IDOR terjadi saat aplikasi memakai identifier dari user tanpa memeriksa kepemilikan. Lihat contoh rentan:
router.get("/v1/orders/:id", requireAuth, async (req, res) => {
const { rows } = await db.query(
"SELECT * FROM orders WHERE id = $1",
[req.params.id],
)
res.json({ data: rows[0] })
})User 5 bisa membaca order milik user lain hanya dengan mengganti id di URL. Validasi kepemilikan hilang — ini access control failure, bukan sekadar bug.
router.get("/v1/orders/:id", requireAuth, async (req, res) => {
const { rows } = await db.query(
`SELECT * FROM orders
WHERE id = $1 AND user_id = $2`,
[req.params.id, req.user.userId],
)
if (rows.length === 0) {
return res.status(404).json({ error: "NOT_FOUND", message: "Order tidak ditemukan" })
}
res.json({ data: rows[0] })
})Dua hal penting:
404, bukan 403 — dengan 404, penyerang tidak bisa membedakan "tidak ada" dari "bukan milik saya" (information disclosure).Note
Pola IDOR sangat umum di API dengan nesting: /users/5/orders/87. Setiap kali ada id dari client, tanyakan: apakah pengguna ini berhak atas resource ini? Jawaban harus di-enforce di query, bukan di UI yang menyembunyikan tombol. Frontend yang menyembunyikan tombol bukan otorisasi (episode 7).
SSRF terjadi saat server memanggil URL yang dikendalikan user — dan serangan klasiknya: server dijadikan pelompat ke resource internal.
Contoh fitur yang rawan: "cek apakah URL gambar valid", "ambil preview link", atau webhook yang menerima URL tujuan.
router.post("/v1/tools/fetch-preview", async (req, res) => {
const { url } = req.body
const response = await fetch(url) // user mengontrol URL!
res.json({ title: extractTitle(await response.text()) })
})Penyerang mengirim http://169.254.169.254/latest/meta-data/ — metadata service cloud yang hanya bisa diakses dari dalam server. Dalam satu request, metadata server (termasuk IAM credentials) bocor.
const ALLOWED_HOSTS = new Set([
"img.shop.example",
"cdn.shop.example",
])
router.post("/v1/tools/fetch-preview", async (req, res) => {
const { url } = req.body
let parsed: URL
try {
parsed = new URL(url)
} catch {
return res.status(400).json({ error: "INVALID_URL" })
}
// 1. Allowlist host
if (!ALLOWED_HOSTS.has(parsed.hostname)) {
return res.status(400).json({ error: "FORBIDDEN_URL" })
}
// 2. Tolak IP internal & metadata
if (isPrivateIp(parsed.hostname)) {
return res.status(400).json({ error: "FORBIDDEN_URL" })
}
const response = await fetch(url, { redirect: "manual" })
res.json({ title: extractTitle(await response.text()) })
})Lapisan mitigasi SSRF:
169.254.169.254).Buat pola review yang sistematis — bukan baca kode sekilas:
Untuk setiap handler, tanyakan: dari mana nilai di req.body, req.params, req.query, dan header? Apa jalurnya sampai ke query/fetch?
- db.query("... " + variabel) → SQL injection
- SELECT * WHERE id = $1 saja → IDOR (tanpa user_id)
- fetch(variabelFromBody) → SSRF
- JSON.stringify(req.body) langsung → no validation# sebagai user 5
TOKEN=$(curl -s -X POST /v1/auth/login -d '{"email":"alice@x.com","password":"x"}' | jq -r .data.token)
curl -s http://localhost:3000/v1/orders/87 -H "Authorization: Bearer $TOKEN"
# coba ganti id → apakah masih 200 untuk order milik orang lain?Tulis test yang sengaja menyerang: ambil resource milik user lain, kirim payload injection, dan minta fetch ke URL internal. Test ini melindungi dari regresi (episode 8).
Tip
Jadikan security review sebagai checklist di pull request (episode 15): adakah input baru? Adakah query baru? Adakah fetch ke URL? Dengan checklist, tiga serangan ini ketahuan di review — bukan di produksi. Tools bantu: SAST (semgrep, CodeQL) untuk pola otomatis, dan scanner OWASP ZAP untuk uji manual.
Selalu 404 saat resource tidak bisa diakses — memberi tahu "ada" lewat 403 adalah bocoran informasi.
Cek rows.length dulu lalu query lagi = race window dan query kedua bisa lupa filter. Satu query dengan filter kepemilikan.
Redirect bisa menembus allowlist host. Set redirect: "manual" dan validasi ulang lokasi baru.
Blacklist ("tolak localhost") selalu bisa dilewati (127.0.0.1, 0177.0.0.1, DNS rebinding). Allowlist ("hanya domain ini") jauh lebih kuat.
Warning
Jangan pernah "memperbaiki" injection dengan escape string manual — selalu ada celah. Satu-satunya mitigasi yang benar untuk SQL injection adalah parameterized query, dan untuk SSRF adalah allowlist + tidak mengikuti redirect. Solusi kreatif manual adalah cara tercepat menambah CVE baru.
Episode 18 membedah tiga serangan paling berbahaya: SQL injection (termasuk blind) yang ditutup dengan parameterized query, IDOR yang memerlukan cek kepemilikan di query, dan SSRF yang ditambal dengan allowlist host dan anti-redirect.
Inti yang harus dibawa pulang:
user_id di query yang sama; balas 404 untuk resource tak berhak.Di episode 19 selanjutnya kita akan melindungi data pengguna: privacy & compliance data — GDPR, enkripsi saat transit dan saat istirahat, serta audit. Sampai jumpa di episode 19!