Webhook adalah jalur masuk data dari sistem eksternal ke aplikasi Anda. Karena sumber eksternal bisa salah konfigurasi, bocor, atau disusupi, endpoint webhook harus dirancang dengan asumsi zero trust: jangan proses apa pun sebelum verifikasi selesai. Konteks ini mirip dengan pelajaran dari gelombang malware di AUR Arch Linux: fakta bahwa sesuatu datang dari ekosistem yang populer atau biasa dipakai bukan alasan untuk langsung mempercayainya.
Untuk verifikasi webhook aman, tiga kontrol inti yang paling sering dibutuhkan adalah: signature untuk memastikan integritas dan keaslian pesan, perlindungan replay untuk mencegah pesan lama dikirim ulang secara jahat, dan idempotensi agar event yang sama tidak diproses berkali-kali saat penyedia melakukan retry. Di praktik nyata, ketiganya harus digabung dengan logging, rotasi secret, validasi payload minimal, dan pemrosesan asinkron yang aman.
Mengapa webhook tidak boleh langsung dipercaya
Kesalahan umum dalam integrasi adalah menganggap bahwa request dari vendor A, payment gateway B, atau platform SaaS C pasti sah. Padahal ada banyak skenario kegagalan:
- Secret webhook bocor di log, repo, atau dashboard yang terlalu longgar aksesnya.
- Request dipalsukan oleh pihak ketiga yang mengetahui format payload.
- Payload valid dikirim ulang untuk memicu efek samping berulang, misalnya membuat invoice atau mengubah status pesanan dua kali.
- Penyedia mengirim retry berkali-kali akibat timeout, sehingga endpoint menerima event yang sama lebih dari sekali.
- Parser membaca body yang sudah dimodifikasi middleware, sehingga verifikasi signature menjadi salah atau tidak bermakna.
Karena itu, endpoint webhook harus diperlakukan sebagai pintu masuk yang berisiko tinggi. Prinsip dasarnya sederhana: terima body mentah, verifikasi, catat, deduplikasi, lalu baru proses.
Model ancaman untuk endpoint webhook
1. Pemalsuan request
Penyerang mengirim request ke endpoint webhook Anda dengan payload yang tampak valid. Jika aplikasi hanya memeriksa struktur JSON atau IP sumber secara longgar, request palsu bisa lolos.
2. Replay attack
Request yang tadinya sah ditangkap lalu dikirim ulang. Tanpa pemeriksaan timestamp dan penyimpanan jejak event, sistem dapat mengeksekusi aksi yang sama berkali-kali.
3. Duplikasi karena retry normal
Tidak semua duplikasi adalah serangan. Banyak penyedia webhook akan melakukan retry jika menerima timeout, koneksi putus, atau status selain 2xx. Endpoint harus aman menghadapi duplikasi yang normal ini.
4. Payload berbahaya atau tidak sesuai ekspektasi
Walau signature valid, payload tetap bisa berisi data yang tidak lengkap, format tidak sesuai, atau nilai yang tidak relevan dengan state sistem Anda. Signature membuktikan asal pesan, bukan bahwa isi pesan aman untuk semua operasi.
Desain alur penerimaan webhook yang aman
Alur berikut cukup umum dan praktis untuk kebanyakan backend:
- Terima request dan ambil raw body sebelum diparsing atau dimodifikasi.
- Ambil header yang diperlukan, misalnya signature, timestamp, dan event ID.
- Hitung ulang signature dari raw body menggunakan secret yang benar.
- Bandingkan signature secara constant-time untuk menghindari timing leak.
- Periksa apakah timestamp masih dalam jendela toleransi.
- Periksa apakah event ID atau idempotency key sudah pernah diproses.
- Simpan catatan penerimaan event beserta status verifikasi dan metadata audit.
- Jika lolos, masukkan event ke queue untuk diproses secara asinkron.
- Kembalikan respons 2xx secepat mungkin setelah event aman disimpan atau dijadwalkan.
Catatan: Bila penyedia mewajibkan acknowledgment cepat, jangan kerjakan logika bisnis berat di thread request. Simpan event yang sudah terverifikasi, beri respons 2xx, lalu proses di worker.
Verifikasi signature berbasis HMAC
Mengapa HMAC dipakai
HMAC memungkinkan dua pihak yang berbagi secret menghitung signature atas isi pesan. Jika body diubah satu byte pun, hasil HMAC berubah. Ini memberi jaminan integritas dan membantu memverifikasi bahwa pengirim mengetahui secret yang sama.
Pola yang umum adalah menghitung HMAC dari gabungan timestamp + "." + raw_body atau langsung dari raw_body, tergantung spesifikasi penyedia. Yang penting: ikuti format yang didokumentasikan penyedia secara persis. Salah urutan, salah separator, atau body sudah diparsing ulang bisa membuat signature mismatch.
Hal yang wajib diperhatikan
- Gunakan raw request body, bukan JSON yang sudah di-serialize ulang.
- Gunakan algoritma yang disepakati, sering kali SHA-256.
- Bandingkan signature dengan fungsi constant-time.
- Jangan log secret atau signature penuh di log aplikasi.
- Bila penyedia mendukung beberapa versi signature, dukung versi yang dibutuhkan secara eksplisit, jangan menebak.
Contoh verifikasi HMAC sederhana
const crypto = require('crypto');
function verifyWebhook({ rawBody, timestamp, receivedSignature, secret }) {
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(receivedSignature, 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
Contoh di atas hanya menunjukkan prinsip dasar. Di implementasi nyata, Anda juga perlu memvalidasi format header, menangani beberapa secret aktif saat rotasi, dan memastikan body tidak berubah oleh middleware.
Kesalahan implementasi yang sering terjadi
- Menghitung HMAC dari objek JSON setelah diparsing, bukan dari body mentah.
- Menormalisasi whitespace atau mengurutkan ulang key JSON sebelum verifikasi.
- Mengambil timestamp dari body padahal spesifikasi mengharuskan dari header.
- Menggunakan perbandingan string biasa daripada constant-time compare.
- Memproses event dulu lalu baru memverifikasi signature.
Timestamp tolerance dan pencegahan replay attack
Mengapa timestamp penting
Signature saja tidak cukup. Jika request sah berhasil disadap atau tercatat di tempat yang salah, request itu bisa dikirim ulang berkali-kali. Dengan menandatangani timestamp dan menolak request yang terlalu lama, Anda mempersempit jendela replay.
Prinsip implementasi
- Ambil timestamp dari header yang ikut ditandatangani.
- Tentukan tolerance window, misalnya beberapa menit, sesuai kebutuhan bisnis dan toleransi clock skew.
- Tolak request dengan timestamp terlalu lama atau terlalu jauh di masa depan.
- Gabungkan dengan penyimpanan event ID atau nonce agar request yang identik dalam window yang sama tetap ditolak saat dikirim ulang.
Contoh pemeriksaan timestamp
function isTimestampValid(timestampSeconds, toleranceSeconds) {
const now = Math.floor(Date.now() / 1000);
const age = Math.abs(now - Number(timestampSeconds));
return age <= toleranceSeconds;
}
Nilai toleransi harus realistis. Terlalu sempit bisa menyebabkan event sah ditolak saat ada keterlambatan jaringan atau clock drift. Terlalu longgar memperbesar peluang replay. Tidak ada angka tunggal yang cocok untuk semua sistem; pilih berdasarkan karakteristik integrasi Anda dan pastikan sinkronisasi waktu server berjalan baik.
Replay protection yang lebih kuat
Timestamp mengurangi risiko, tetapi pencegahan replay yang lebih kuat biasanya membutuhkan penyimpanan jejak request unik, misalnya berdasarkan:
- event ID dari penyedia, jika dijamin unik.
- delivery ID atau message ID per pengiriman.
- hash dari signature + timestamp + body jika tidak ada ID unik yang dapat diandalkan.
Simpan jejak ini dengan TTL yang sesuai di Redis atau database cepat. Jika kunci yang sama sudah ada, anggap request sebagai duplikat atau replay.
Idempotensi, event ID, dan deduplikasi
Perbedaan replay dan duplikasi normal
Replay attack adalah pengiriman ulang yang tidak sah atau tidak diinginkan. Duplikasi normal terjadi karena retry dari penyedia. Dari sisi endpoint, keduanya sering terlihat sama: event yang sama datang lebih dari sekali. Karena itu, idempotensi wajib ada walau signature dan timestamp sudah benar.
Apa itu idempotensi dalam webhook
Idempotensi berarti memproses event yang sama berulang kali tidak menghasilkan efek samping lebih dari sekali. Misalnya, event payment.succeeded yang sama tidak boleh membuat dua transaksi internal, dua email konfirmasi, atau dua perubahan status yang saling bertabrakan.
Strategi deduplikasi yang umum
- Gunakan event ID dari penyedia sebagai kunci unik, jika tersedia dan stabil.
- Simpan status pemrosesan: received, verified, processing, processed, failed.
- Buat unique constraint di database pada event ID atau kombinasi sumber + event ID.
- Gunakan Redis SETNX atau mekanisme serupa untuk menandai event yang sudah pernah dijadwalkan.
- Lindungi operasi bisnis dengan unique key tambahan, misalnya transaction reference atau external payment ID.
Contoh tabel event inbox
webhook_inbox
- id
- provider
- event_id
- delivery_id
- received_at
- verified_at
- status -- received | verified | processing | processed | failed
- payload_hash
- payload_raw
- signature_meta
- processed_at
- error_message
UNIQUE(provider, event_id)
Pola ini sering disebut inbox table. Ia membantu deduplikasi, audit, retry internal, dan investigasi insiden. Menyimpan payload_raw bisa sangat berguna untuk debugging, tetapi pertimbangkan kebijakan retensi dan masking jika payload mengandung data sensitif.
Idempotensi di level logika bisnis
Deduplikasi di level event belum tentu cukup. Misalnya, dua event berbeda bisa sama-sama mencoba membuat entitas bisnis yang sama. Karena itu, operasi domain inti juga perlu dirancang idempotent, contohnya:
UPSERTberdasarkan external reference.- Pemeriksaan state transition yang sah, misalnya hanya izinkan perubahan dari
pendingkepaidsekali. - Unique constraint pada nomor transaksi eksternal.
Retry yang aman dan respons HTTP
Kapan mengembalikan 2xx
Jika request sudah terverifikasi dan sudah disimpan aman untuk diproses, biasanya aman untuk mengembalikan 2xx meskipun pemrosesan bisnis dilakukan di belakang layar. Ini mencegah retry yang tidak perlu dari penyedia.
Kapan mengembalikan 4xx atau 5xx
- 4xx untuk request yang memang tidak valid, misalnya signature salah, header wajib hilang, atau timestamp di luar toleransi.
- 5xx untuk kegagalan sementara di sisi Anda, misalnya database atau queue tidak tersedia, sehingga penyedia boleh retry.
Jangan membalas 2xx jika event belum berhasil diamankan, misalnya belum tersimpan ke database/queue, karena Anda berisiko kehilangan event tanpa kesempatan retry.
Pola penerimaan yang praktis
- Verifikasi request.
- Simpan ke inbox table dalam satu transaksi dengan constraint unik.
- Jika duplikat, kembalikan 2xx agar penyedia berhenti retry.
- Jika baru, enqueue job pemrosesan.
- Worker memproses secara idempotent dan memperbarui status.
Secret rotation tanpa downtime
Secret webhook tidak boleh dianggap permanen. Ia bisa perlu diganti karena kebocoran, pergantian vendor, atau kebijakan keamanan internal. Masalahnya, rotasi yang kasar dapat menyebabkan semua webhook gagal verifikasi selama masa transisi.
Strategi rotasi yang aman
- Simpan lebih dari satu secret aktif per provider selama periode transisi.
- Saat verifikasi, coba secret utama, lalu secret cadangan yang masih aktif.
- Catat secret mana yang cocok untuk audit, tanpa menyimpan nilainya.
- Setelah semua pengirim dipastikan memakai secret baru, nonaktifkan secret lama.
Contoh logika verifikasi multi-secret
function verifyWithAnySecret({ rawBody, timestamp, receivedSignature, secrets }) {
for (const secretMeta of secrets) {
const ok = verifyWebhook({
rawBody,
timestamp,
receivedSignature,
secret: secretMeta.value,
});
if (ok) {
return { ok: true, keyId: secretMeta.keyId };
}
}
return { ok: false };
}
Idealnya setiap secret punya identifier internal sendiri agar audit lebih mudah. Jika penyedia mendukung penandaan key di header, manfaatkan itu untuk mempersempit secret yang perlu dicoba.
Audit logging dan observability
Ketika webhook gagal, pertanyaan pertama biasanya: request masuk atau tidak, signature cocok atau tidak, duplikat atau tidak, dan gagal di tahap mana. Tanpa logging yang baik, debugging akan lambat dan rawan salah asumsi.
Apa yang sebaiknya dicatat
- Waktu diterima.
- Provider atau sumber webhook.
- Event ID, delivery ID, atau request correlation ID.
- Status verifikasi: sukses/gagal dan alasannya.
- Status deduplikasi: baru/duplikat/replay.
- Status enqueue dan status pemrosesan worker.
- Hash payload atau metadata penting untuk korelasi.
Apa yang tidak sebaiknya dicatat
- Secret webhook.
- Signature penuh jika tidak perlu.
- Payload sensitif tanpa masking atau kontrol retensi.
Tip debugging: jika signature sering mismatch, periksa apakah framework Anda mengubah body request, menambahkan newline, mengonversi encoding, atau mem-parsing JSON sebelum verifikasi dilakukan.
Validasi payload minimal setelah verifikasi
Setelah signature lolos, jangan langsung menganggap semua field di payload aman dipakai. Verifikasi kriptografis memastikan pesan berasal dari pihak yang mengetahui secret, tetapi tidak menggantikan validasi input.
Validasi minimal yang sebaiknya ada
- Pastikan field identitas event tersedia, misalnya event ID dan event type.
- Pastikan field domain penting ada, misalnya external order ID atau payment ID.
- Batasi ukuran payload agar endpoint tidak menjadi sasaran payload besar.
- Tolak tipe event yang tidak Anda dukung.
- Validasi format dasar tanpa melakukan transformasi berlebihan.
Validasi ini sebaiknya minimal namun tegas. Tujuannya bukan menyusun ulang payload vendor, tetapi memastikan hanya data yang relevan dan aman yang masuk ke pipeline bisnis Anda.
Contoh alur implementasi end-to-end
POST /webhooks/provider
1. Ambil raw body dan header signature/timestamp/event-id
2. Jika header wajib hilang -> 400
3. Jika timestamp di luar toleransi -> 400
4. Verifikasi HMAC dengan secret aktif/sekunder
5. Jika signature gagal -> 401 atau 400 sesuai kebijakan
6. Hitung fingerprint/event key untuk replay & deduplikasi
7. Simpan ke webhook_inbox dengan unique constraint
8. Jika duplicate key -> return 200/204
9. Enqueue job pemrosesan
10. Return 200/202
11. Worker memproses event secara idempotent
12. Update status processed/failed + audit log
Di banyak sistem, langkah 7 dan 9 sebaiknya terjadi dalam pola yang menjamin event tidak hilang, misalnya menyimpan record inbox terlebih dahulu lalu worker mengambil dari sana. Jika Anda langsung enqueue tanpa jejak persistensi, debugging dan recovery akan lebih sulit.
Edge case umum yang sering terlewat
- Clock skew antara server Anda dan server penyedia membuat request sah terlihat kedaluwarsa.
- Body parser framework mengubah payload sebelum verifikasi.
- Compression atau proxy memengaruhi body yang diverifikasi jika tidak dipahami dengan benar.
- Retry datang bersamaan dan memicu race condition pada deduplikasi jika tidak ada unique constraint.
- Out-of-order delivery: event status lama datang setelah status baru.
- Secret rotation belum sinkron sehingga sebagian request masih ditandatangani dengan secret lama.
- Timeout worker menyebabkan pemrosesan internal ganda bila job diulang tanpa idempotensi domain.
- Payload valid tapi referensi domain belum ada, misalnya order belum tersinkron saat event pembayaran tiba.
Menangani out-of-order delivery
Tidak semua penyedia menjamin urutan event. Karena itu, logika bisnis tidak boleh hanya mengandalkan urutan kedatangan. Simpan versi state atau cek transisi yang valid. Misalnya, jangan biarkan event pending menimpa status paid yang sudah lebih baru.
Anti-pattern yang perlu dihindari
- Hanya mengandalkan IP allowlist. Ini bisa membantu sebagai lapisan tambahan, tetapi bukan bukti keaslian payload. IP bisa berubah, ada proxy/CDN, dan jika payload valid pernah bocor, replay tetap mungkin terjadi.
- Memproses event sebelum verifikasi. Ini membuka celah besar untuk request palsu dan efek samping yang sulit dibatalkan.
- Menganggap 2xx selalu aman dikirim cepat. Jika event belum tersimpan aman, Anda bisa kehilangan data permanen.
- Tidak menyimpan event ID atau fingerprint. Signature dan timestamp tanpa deduplikasi tidak cukup menghadapi retry dan replay.
- Menghapus detail log terlalu agresif. Investigasi insiden webhook butuh jejak yang cukup, walau tetap harus menjaga data sensitif.
- Menganggap signature valid berarti payload pasti cocok dengan state bisnis. Tetap perlu validasi domain dan kontrol transisi state.
Checklist implementasi verifikasi webhook aman
- Ambil raw body sebelum parsing.
- Verifikasi signature HMAC sesuai format vendor.
- Gunakan constant-time compare.
- Periksa timestamp tolerance dan sinkronisasi waktu server.
- Simpan event ID/delivery ID/fingerprint untuk replay protection.
- Terapkan idempotensi di level event dan logika bisnis.
- Gunakan unique constraint atau kunci atomik untuk deduplikasi.
- Kembalikan 2xx hanya setelah event aman tersimpan.
- Proses berat di queue/worker, bukan di request thread.
- Dukung secret rotation dengan beberapa secret aktif sementara.
- Catat audit log yang cukup tanpa membocorkan secret.
- Lakukan validasi payload minimal setelah verifikasi.
- Uji duplikasi, replay, out-of-order, dan kegagalan dependency secara eksplisit.
Penutup
Pelajaran dari insiden supply chain adalah sederhana: sumber eksternal tidak boleh dipercaya secara implisit. Dalam konteks API dan webhook, itu berarti jangan pernah memproses request hanya karena ia datang dari integrasi yang dikenal. Verifikasi webhook aman membutuhkan kombinasi signature HMAC, pembatasan timestamp, perlindungan replay, idempotensi, dan deduplikasi yang benar.
Jika Anda hanya mengambil satu prinsip dari artikel ini, gunakan yang ini: verify first, persist safely, process once. Dengan pola itu, endpoint webhook Anda akan jauh lebih tahan terhadap serangan, retry, duplikasi, dan gangguan integrasi yang memang sering terjadi di sistem nyata.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!