API security bukan satu fitur, tapi daftar periksa: setiap pintu masuk harus dikunci, karena penyerang cukup menemukan satu yang lupa. Bayangin satpam gedung kantor dengan checklist. Semua orang masuk lewat pintu depan yang dijaga. Setiap tamu menunjukkan kartu identitas. Tamu cuma boleh masuk ke lantai yang dia tuju, bukan semua ruangan. Barang bawaannya dicek. Orang yang mencoba masuk 100 kali per menit dihentikan. Saat alarm berbunyi, satpam gak membacakan denah gedung lewat pengeras suara. Dan paket cuma diserahkan ke kurir yang ada di daftar.
🧪 Tebakan cepat: Budi login dengan token yang sah, lalu memanggil
GET /orders/2, padahal pesanan nomor 2 milik Siti. Server mengecek token-nya: sah. Apa yang seharusnya dijawab server? Tebak dulu, jawabannya ada di bagian "Object-level authorization".
Daftar periksa API Toko Buku
Tujuh pemeriksaan, masing-masing dengan request contoh dari buku teks dan jawaban yang aman:
| # | Pemeriksaan | Request contoh | Jawaban yang aman |
|---|---|---|---|
| 1 | HTTPS saja | GET http://api.tokobuku.test/orders/1 |
ditolak tanpa data; jawaban HTTPS membawa Strict-Transport-Security |
| 2 | Auth di setiap route | DELETE /books/1 tanpa token |
401 Unauthorized |
| 3 | Object-level authorization | Budi: GET /orders/2 (milik Siti) |
404 Not Found |
| 4 | Validasi input | POST /books dengan "price": -5000 |
422 |
| 5 | Rate limit di login | 10 kali POST /login dengan password salah |
429 mulai percobaan ke-6 |
| 6 | Tanpa stack trace | GET /books/abc yang memicu bug |
500 dengan {"error": "Internal Server Error"} saja |
| 7 | CORS dengan allowlist | GET /orders dari halaman https://evil.test |
tanpa Access-Control-Allow-Origin: https://evil.test |
- HTTPS saja. Tanpa TLS, token di header
Authorizationbisa dibaca siapa pun di Wi-Fi yang sama. API sebaiknya gak melayanihttp://sama sekali: kalau dialihkan, token di request pertama sudah terlanjur lewat tanpa enkripsi. HeaderStrict-Transport-Security: max-age=31536000; includeSubDomains(HSTS) membuat browser selalu memakai HTTPS. - Auth di setiap route. Satu route yang lupa
requireAuthcukup untuk menghapus buku tanpa login. Pasang middleware-nya di semua route yang butuh login, bukan satu per satu sambil berharap gak ada yang kelewat. - Rate limit di login. Batasi per email dan per IP, misalnya 5 percobaan per 15 menit (lihat pelajaran Rate Limiting).
Object-level authorization
Token membuktikan siapa yang memanggil (autentikasi). Itu belum menjawab apakah orang itu boleh membaca objek yang dia minta (otorisasi). Server yang cuma mengecek token akan memberikan alamat rumah Siti ke Budi. Ini masalah nomor 1 di OWASP API Security Top 10 2023: Broken Object Level Authorization.
app.get("/orders/:id", requireAuth, (req, res) => {
const order = orders.find((o) => o.id === Number(req.params.id));
if (!order || order.user_id !== req.user.id) {
return res.status(404).json({ error: "Order not found" });
}
res.json(order);
});
requireAuthmemastikan ada user yang login (req.user).order.user_id !== req.user.idmemastikan pesanan itu miliknya.- Jawabannya
404, sama dengan pesanan yang memang gak ada, supaya Budi gak bisa menebak nomor pesanan orang lain yang ada.
Jadi jawaban yang benar untuk Budi adalah 404 (atau 403), bukan pesanan Siti. Itu jawaban tebakannya.
CORS: daftar origin yang boleh
Browser memblokir halaman dari origin lain membaca jawaban API-mu, kecuali server mengizinkannya lewat header Access-Control-Allow-Origin. Mekanisme ini namanya CORS (Cross-Origin Resource Sharing).
app.use(cors({
origin: ["https://tokobuku.test"], // allowlist, bukan semua origin
credentials: true,
}));
Kesalahannya: memantulkan origin apa pun (origin: true) sambil mengizinkan credentials. Halaman https://evil.test yang dibuka user yang sedang login bisa membaca pesanannya. CORS cuma membatasi browser; curl atau server lain tetap bisa memanggil API-mu, jadi CORS bukan pengganti auth.
Input: validasi, lalu query pakai parameter
Semua yang datang dari client dicek dulu: tipe, rentang, panjang. Harga -5000 ditolak 422. Lalu input gak pernah ditempel ke dalam teks query:
// SQL injection: input ditempel langsung ke query
db.query(`SELECT * FROM u WHERE id = ${req.body.id}`);
// aman: parameterized query, nilainya dikirim terpisah
db.query("SELECT * FROM u WHERE id = ?", [req.body.id]);
Kalau req.body.id berisi 1 OR 1=1, versi pertama mengembalikan semua user. Versi kedua memperlakukannya sebagai satu nilai.
Latihan membuat pembersih teks sederhana untuk field teks biasa:
function sanitizeInput(input) {
const withoutTags = String(input).replace(/<[^>]*>/g, "");
const trimmed = withoutTags.trim();
return trimmed === "" ? null : trimmed;
}
| Input | Hasil |
|---|---|
"<script>alert(1)</script>hello" |
alert(1)hello |
"O'Brien" |
O'Brien |
" <b>bold</b> " |
bold |
"<p></p>" |
null |
"plain text" |
plain text |
replace(/<[^>]*>/g, "")membuang semua tag, isinya tetap tinggal.trim()membuang spasi di awal dan akhir, setelah tag-nya dibuang; hasil kosong jadinull.- Kutip dibiarkan. Parameterized query sudah menyimpan
O'Briendengan aman; pembersih yang menggandakannya malah menyimpanO''Brien.
Pembersih seperti ini cuma lapisan tambahan. Pertahanan utamanya tetap parameterized query untuk SQL, dan escape saat menampilkan untuk XSS (React melakukannya otomatis).
Error tanpa stack trace, dan helmet
app.use((err, req, res, next) => {
console.error(err); // detailnya cuma di log server
res.status(500).json({ error: "Internal Server Error" });
});
Stack trace di jawaban membocorkan nama file, versi library, dan isi query ke penyerang. Simpan di log, kirim pesan umum ke client.
Untuk header keamanan, Express punya middleware helmet: app.use(helmet()) sekali saja memasang belasan header aman, termasuk Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options, dan Content-Security-Policy, dan menghapus X-Powered-By.
🤔 Cara serangannya bekerja
Pelajaran ini daftar periksa untuk pihak yang bertahan. Cara tiap serangan bekerja dibahas di track Security: "SQL Injection Prevention", "XSS Prevention & Sanitization", "CORS Deep Dive", "API Authentication Patterns", "Rate Limiting & Throttling", dan "Security Headers Lengkap".
⚠️ Jebakan yang sering ketemu
- Mengira token sah berarti boleh. Cek juga kepemilikan objeknya di setiap route yang menerima id.
Access-Control-Allow-Originyang memantulkan origin apa pun dengan credentials. Pakai allowlist.- Menempel input ke query. Template string berisi
req.bodydi dalam SQL itu SQL injection. Pakai parameter. - Mengirim
err.stackatauerr.messagementah ke client. Kirim pesan umum, log detailnya.
TL;DR: Periksa tujuh hal: HTTPS saja (plus HSTS), auth di setiap route, object-level authorization (token sah belum tentu boleh membaca pesanan orang lain: jawab 404), validasi input (422), rate limit di login (429), error 500 tanpa stack trace, dan CORS dengan allowlist origin, bukan memantulkan semua origin. CORS adalah aturan browser yang memblokir request lintas origin kecuali server mengirim Access-Control-Allow-Origin. Input yang ditempel ke query adalah SQL injection; pakai parameterized query. helmet memasang header keamanan sekaligus.