Mencegah replay webhook berarti memastikan request yang pernah valid tidak bisa dipakai ulang di luar konteks yang sah. Cara yang umum dan efektif adalah menggabungkan signature HMAC, timestamp, dan mekanisme deduplikasi seperti event ID atau nonce. Ketiganya saling melengkapi: signature membuktikan keaslian dan integritas payload, timestamp membatasi umur request, dan deduplikasi mencegah eksekusi ganda.
Jika Anda hanya memverifikasi signature tanpa timestamp atau tanpa penyimpanan identitas request, request yang berhasil disadap dari log, proxy, atau sistem internal masih bisa diputar ulang. Sebaliknya, timestamp tanpa signature tidak berguna karena penyerang bisa membuat timestamp baru. Desain yang aman harus memvalidasi semuanya dalam urutan yang benar sebelum payload diproses.
Masalah yang ingin diselesaikan
Webhook biasanya memicu operasi yang memiliki efek samping: membuat order, mengubah status pembayaran, mengirim email, atau menyinkronkan data. Jika event yang sama diproses lebih dari sekali, hasilnya bisa berupa duplikasi transaksi, status yang lompat, atau data yang tidak konsisten.
Replay attack berbeda dari retry sah. Provider webhook yang baik memang akan mengirim ulang event saat endpoint Anda timeout, gagal, atau mengembalikan status non-2xx. Sistem Anda harus bisa:
- Menolak replay berbahaya dari request lama atau request yang diubah.
- Menerima retry sah dari provider untuk event yang sama tanpa menimbulkan efek samping ganda.
- Memberi respons deterministik agar integrasi mudah dioperasikan dan di-debug.
Kontrak request webhook yang tahan replay
Langkah pertama adalah mendefinisikan kontrak request yang jelas antara provider dan consumer. Tanpa kontrak ini, verifikasi mudah rusak karena perbedaan format body, header, atau cara menghitung signature.
Elemen minimal yang sebaiknya ada
- Raw request body sebagai input utama signature. Jangan menandatangani JSON yang sudah diparse lalu diserialisasi ulang.
- Timestamp dalam header, misalnya waktu Unix detik.
- Event ID unik dan stabil untuk satu event logis.
- Signature berbasis HMAC menggunakan shared secret.
- Algoritme yang eksplisit, misalnya HMAC-SHA256.
Contoh kontrak header yang sederhana:
Webhook-Id: evt_01HXYZABC123
Webhook-Timestamp: 1715157000
Webhook-Signature: v1=5f2c0d8e...
Content-Type: application/jsonContoh payload string yang ditandatangani:
{timestamp}.{raw_body}Lalu signature dibuat sebagai:
hex(HMAC_SHA256(secret, timestamp + "." + raw_body))Catatan: Memasukkan timestamp ke string yang ditandatangani penting agar penyerang tidak bisa mengganti timestamp tanpa membuat signature menjadi tidak valid.
Skema signature HMAC yang benar
HMAC cocok untuk webhook karena sederhana, cepat, dan tidak memerlukan infrastruktur kunci publik. Provider dan consumer berbagi secret yang sama, lalu consumer menghitung ulang signature dari data mentah yang diterima.
Mengapa harus memakai raw body
Kesalahan paling umum adalah menghitung HMAC dari hasil parsing JSON. Ini berbahaya karena representasi JSON bisa berubah tanpa mengubah makna data, misalnya urutan properti, spasi, format angka, atau newline. Jika proxy, middleware, atau framework memodifikasi body sebelum verifikasi, signature akan gagal meskipun request aslinya valid.
Prinsipnya:
- Ambil body mentah persis seperti yang diterima di socket atau request stream.
- Simpan body mentah itu untuk verifikasi signature.
- Parse JSON setelah signature lolos.
Contoh verifikasi HMAC
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expected = hmacSha256Hex(secret, signedPayload);
return constantTimeEqual(`v1=${expected}`, receivedSignature);
}Gunakan constant-time comparison agar perbandingan signature tidak bocor lewat timing attack. Di banyak bahasa pemrograman tersedia fungsi bawaan untuk ini; jika ada, gunakan itu daripada membuat implementasi sendiri.
Rotasi secret
Pada sistem produksi, secret akan perlu dirotasi. Strategi praktis:
- Simpan secret aktif dan secret sebelumnya selama masa transisi.
- Coba verifikasi dengan secret aktif terlebih dahulu, lalu dengan secret lama jika gagal.
- Catat secret mana yang berhasil, tetapi jangan pernah menulis nilai secret ke log.
Ini membantu migrasi tanpa downtime saat provider dan consumer belum berganti secret secara serentak.
Validasi timestamp dan window toleransi waktu
Timestamp dipakai untuk membatasi seberapa lama request dianggap valid. Tanpa batas waktu, request yang pernah valid bisa diputar ulang kapan saja selama signature masih sama.
Aturan dasar
- Tolak request jika header timestamp tidak ada atau formatnya tidak valid.
- Bandingkan timestamp request dengan waktu server saat ini.
- Terima hanya jika selisih waktunya berada dalam window toleransi yang ditentukan.
Contoh logika sederhana:
function isTimestampFresh(timestamp, now, toleranceSeconds) {
const age = Math.abs(now - timestamp);
return age <= toleranceSeconds;
}Berapa toleransi waktu yang aman?
Tidak ada angka universal untuk semua sistem, tetapi window harus cukup sempit untuk membatasi replay dan cukup longgar untuk mengakomodasi clock skew, antrean jaringan, dan retry singkat. Banyak implementasi memakai toleransi dalam hitungan menit, bukan jam. Jika provider Anda mengirim dari infrastruktur global atau lewat antrean internal, gunakan angka yang realistis berdasarkan perilaku aktual sistem, bukan asumsi.
Pertimbangan memilih toleransi:
- Terlalu sempit: request sah gagal saat ada clock skew atau antrean sementara.
- Terlalu longgar: replay window makin besar dan risiko meningkat.
- Harus konsisten: timestamp yang sudah lewat toleransi sebaiknya ditolak sebelum payload diproses lebih jauh.
Clock skew dan sinkronisasi waktu
Validasi timestamp bergantung pada jam server. Jika server Anda meleset, request sah bisa ditolak atau replay lama bisa lolos. Pastikan host atau container runtime tersinkron dengan NTP atau layanan sinkronisasi waktu yang setara.
Untuk observability, log-kan:
- timestamp dari request,
- waktu server saat verifikasi,
- selisih detik,
- keputusan accept/reject.
Praktik baik: Jika selisih waktu sering mendekati batas toleransi, masalahnya biasanya bukan pada kriptografi, tetapi pada sinkronisasi jam, antrean provider, atau arsitektur proxy di depan aplikasi.
Nonce, event ID, dan deduplikasi
Timestamp membatasi umur request, tetapi belum cukup untuk mencegah replay di dalam window yang masih valid. Karena itu Anda perlu deduplikasi.
Pilihan 1: event ID
Jika provider mengirim event ID yang unik dan stabil, ini biasanya pilihan terbaik. Simpan event ID yang sudah diproses, lalu tolak atau abaikan event yang sama saat datang lagi.
Kelebihan event ID:
- Sederhana dipahami dan dioperasikan.
- Mudah mendukung retry sah dari provider.
- Cocok untuk idempotensi pada level event bisnis.
Kekurangannya:
- Anda bergantung pada kualitas provider: event ID harus benar-benar unik dan stabil.
- Jika provider mengirim ulang event yang sama dengan body sedikit berbeda, Anda perlu kebijakan yang jelas apakah itu dianggap valid atau anomali.
Pilihan 2: nonce
Nonce adalah nilai acak unik per request yang ditandatangani bersama payload. Consumer menyimpan nonce yang pernah dipakai selama masa berlaku tertentu. Ini lebih umum bila Anda mengontrol kedua sisi protokol atau saat event ID tidak tersedia.
Kelebihan nonce:
- Tidak bergantung pada konsep event bisnis.
- Efektif mencegah request identik dipakai ulang dalam rentang waktu tertentu.
Kekurangannya:
- Memerlukan storage cepat dengan TTL.
- Retry sah bisa tertolak jika provider mengirim nonce yang sama tetapi Anda tidak membedakan antara retry dan replay. Desainnya harus eksplisit.
Pilihan 3: hash request
Alternatif lain adalah menyimpan hash dari request yang diterima, misalnya hash dari timestamp.raw_body atau bagian tertentu yang relevan. Ini berguna ketika provider tidak menyediakan event ID.
Kelebihan:
- Tidak perlu perubahan protokol jika data yang ada sudah cukup.
- Dapat mendeteksi request identik yang masuk berulang.
Kekurangan dan trade-off:
- Retry sah dengan payload yang sama akan terlihat sebagai duplikat; Anda harus memutuskan apakah akan di-ack sebagai sukses atau ditolak secara eksplisit.
- Sangat sensitif terhadap perubahan kecil pada body. Bila provider mengubah serialisasi JSON, hash akan berbeda meskipun event bisnisnya sama.
- Kurang ideal untuk idempotensi bisnis dibanding event ID.
Mana yang dipilih?
Secara umum:
- Pilih event ID jika provider menyediakannya dan nilainya stabil.
- Pilih nonce jika Anda mendesain protokol sendiri dan ingin anti-replay per request.
- Gunakan hash request sebagai fallback saat event ID tidak ada, sambil menerima keterbatasannya.
Urutan verifikasi yang benar sebelum memproses payload
Urutan ini penting untuk keamanan dan efisiensi. Jangan parse payload atau menyentuh database bisnis sebelum request lulus pemeriksaan dasar.
- Baca raw body tanpa modifikasi.
- Ambil header wajib: signature, timestamp, dan event ID atau nonce jika ada.
- Validasi format dasar: header ada, timestamp numerik, format signature sesuai.
- Validasi timestamp terhadap toleransi waktu.
- Verifikasi signature HMAC dengan raw body dan timestamp.
- Cek deduplikasi: event ID, nonce, atau hash request.
- Parse payload setelah lolos verifikasi.
- Proses bisnis secara idempotent.
- Simpan status pemrosesan dan kembalikan respons yang sesuai.
Mengapa timestamp divalidasi sebelum HMAC? Karena request yang sudah terlalu tua tidak perlu memakan sumber daya lebih jauh. Namun, ada juga implementasi yang menghitung HMAC lebih dulu untuk menghindari memberikan sinyal yang berbeda kepada penyerang. Dua pendekatan ini sama-sama mungkin, tetapi untuk webhook internal aplikasi, urutan di atas sering dipilih karena pragmatis. Yang penting adalah respons error tidak membocorkan detail sensitif.
Contoh pseudo-code endpoint
function handleWebhook(request) {
const rawBody = request.rawBody;
const signature = request.header('Webhook-Signature');
const timestampHeader = request.header('Webhook-Timestamp');
const eventId = request.header('Webhook-Id');
if (!rawBody || !signature || !timestampHeader) {
return errorResponse(400, 'invalid_webhook');
}
const timestamp = parseUnixTimestamp(timestampHeader);
if (!timestamp) {
return errorResponse(400, 'invalid_webhook');
}
if (!isTimestampFresh(timestamp, unixNow(), 300)) {
return errorResponse(401, 'invalid_webhook');
}
if (!verifySignature(rawBody, timestampHeader, signature, secret)) {
return errorResponse(401, 'invalid_webhook');
}
if (eventId && hasProcessedEvent(eventId)) {
return successResponse(200, 'duplicate_ignored');
}
const payload = parseJson(rawBody);
processEventIdempotently(eventId, payload);
markEventProcessed(eventId);
return successResponse(200, 'ok');
}Perhatikan bahwa duplicate yang sudah pernah diproses bisa tetap dikembalikan sebagai 200 agar provider berhenti retry. Ini umumnya lebih praktis daripada mengembalikan error.
Menangani retry sah dari provider
Retry sah harus dianggap normal, bukan anomali. Penyebabnya bisa timeout, respons 5xx, kegagalan jaringan, atau provider belum menerima ACK tepat waktu.
Prinsip desain
- Verifikasi keamanan tetap dilakukan pada setiap retry.
- Idempotensi harus ada di level bisnis, bukan hanya di gerbang webhook.
- Duplicate yang valid sebaiknya direspons sukses jika event tersebut sudah diproses dengan benar sebelumnya.
Strategi umum:
- Simpan status event: received, processing, processed, failed.
- Gunakan transaksi database atau mekanisme lock untuk mencegah dua worker memproses event yang sama bersamaan.
- Jika memproses secara asynchronous, ACK request setelah event aman disimpan ke antrean internal atau tabel inbox, bukan setelah seluruh proses bisnis selesai, sesuai kebutuhan konsistensi Anda.
Inbox pattern untuk webhook
Pola yang sering dipakai adalah menyimpan event tervalidasi ke tabel inbox terlebih dahulu. Endpoint hanya bertugas memverifikasi, mendeduplikasi, dan menyimpan. Worker terpisah lalu memproses bisnis dari inbox tersebut.
Keuntungannya:
- Respons endpoint cepat dan stabil.
- Retry dari provider lebih mudah ditangani.
- Audit trail lebih lengkap untuk debugging.
Komprominya:
- Arsitektur lebih kompleks.
- Butuh kebijakan retry internal dan monitoring worker.
Edge case yang sering membuat verifikasi gagal
1. Clock skew
Server consumer atau provider memiliki jam yang meleset. Gejalanya adalah signature valid tetapi timestamp dianggap kedaluwarsa atau terlalu jauh di masa depan.
Yang perlu dilakukan:
- Pastikan sinkronisasi waktu sistem.
- Log selisih waktu secara eksplisit.
- Jika perlu, sesuaikan toleransi berdasarkan data produksi, bukan tebakan.
2. Body berubah karena proxy atau middleware
Misalnya proxy menormalisasi line ending, middleware mengompresi/dekompresi, atau framework membaca stream lalu mengganti representasi body. Hasilnya, HMAC yang Anda hitung tidak cocok dengan yang dihitung provider.
Mitigasi:
- Verifikasi dari raw body asli.
- Nonaktifkan transformasi body pada route webhook jika framework memungkinkan.
- Jangan bangun ulang JSON untuk verifikasi.
3. Header hilang atau namanya berubah
Beberapa proxy atau load balancer dapat memodifikasi atau memfilter header tertentu. Ada juga perbedaan penamaan antara huruf besar-kecil atau awalan khusus.
Mitigasi:
- Gunakan nama header yang jelas dan terdokumentasi.
- Pastikan reverse proxy meneruskan header tersebut.
- Di log, catat kehadiran header, bukan nilainya secara penuh.
4. Event dikirim ulang dengan payload sama
Ini sering merupakan retry sah. Jika event ID sama dan status sudah processed, kembalikan 200 dan jangan proses ulang efek samping.
5. Event ID sama tetapi payload berbeda
Ini adalah kondisi yang harus dianggap mencurigakan. Bisa jadi bug provider, korupsi data, atau percobaan manipulasi.
Respons yang disarankan:
- Jangan proses ulang secara otomatis.
- Catat sebagai anomali keamanan/integrasi.
- Simpan fingerprint body untuk investigasi.
6. Request sangat lambat tiba
Jika provider mengantre event lama lalu mengirimkannya saat sistem pulih, event bisa gagal karena timestamp di luar toleransi. Ini keputusan desain: apakah keamanan anti-replay lebih penting daripada menerima event terlambat. Banyak sistem memilih menolak event kadaluwarsa dan menyelesaikan data lewat rekonsiliasi atau API pull terpisah.
Format error yang aman
Jangan membocorkan alasan rinci seperti “signature salah” atau “timestamp lewat 742 detik” dalam body respons publik. Informasi detail itu berguna bagi operator Anda, tetapi juga membantu penyerang memahami gerbang verifikasi.
Format respons aman yang sederhana:
{
"error": "invalid_webhook"
}Status code yang umum:
- 400 untuk request malformed, misalnya header wajib hilang atau format tidak valid.
- 401 atau 403 untuk verifikasi gagal, misalnya signature tidak cocok atau timestamp di luar toleransi.
- 200 untuk duplicate yang sudah pernah diproses dan aman diabaikan.
- 500 hanya jika kegagalan ada di pihak Anda setelah verifikasi lolos.
Detail teknis lengkap simpan di log internal, bukan di respons HTTP.
Observability dan debugging integrasi
Webhook yang aman tetapi sulit di-debug akan tetap mahal dioperasikan. Anda perlu observability yang cukup untuk membedakan masalah keamanan dari masalah integrasi biasa.
Apa yang perlu dicatat di log
- request_id internal.
- event_id atau nonce jika ada.
- provider atau sumber webhook.
- timestamp_request dan timestamp_server.
- clock_skew_seconds.
- signature_present dan signature_version.
- verification_result: missing_header, stale_timestamp, bad_signature, duplicate, accepted.
- payload_hash atau fingerprint aman, bukan payload mentah penuh jika sensitif.
Hindari menulis secret, signature penuh, atau data sensitif pelanggan ke log. Jika perlu, simpan hanya prefix hash atau versi yang sudah dimasking.
Metrics yang berguna
- Jumlah request webhook per provider.
- Persentase gagal verifikasi.
- Distribusi clock skew.
- Jumlah duplicate event.
- Latency endpoint verifikasi.
- Jumlah event masuk antrean vs berhasil diproses.
Tracing dan korelasi
Jika endpoint webhook hanya memasukkan event ke queue atau inbox, buat korelasi antara request awal, record inbox, job queue, dan proses bisnis akhir. Dengan begitu Anda bisa menjawab pertanyaan seperti: “event sudah diverifikasi?”, “sudah masuk queue?”, “kenapa belum mengubah status order?”
Contoh desain penyimpanan deduplikasi
Berbasis event ID
Gunakan tabel atau key-value store dengan key unik pada provider + event_id. Simpan minimal:
- provider,
- event_id,
- received_at,
- payload_hash,
- status processing,
- processed_at.
Keuntungan utama pendekatan ini adalah mudah diaudit dan cocok dengan retry sah.
Berbasis hash request
Gunakan hash yang berasal dari data yang sudah diverifikasi, misalnya SHA256(timestamp + "." + raw_body) atau fingerprint lain yang stabil. Simpan hash dengan TTL jika hanya untuk anti-replay jangka pendek.
Pendekatan ini lebih cocok untuk:
- provider tanpa event ID,
- protokol internal,
- skenario di mana Anda hanya perlu mencegah request identik dalam window tertentu.
Namun untuk idempotensi bisnis jangka panjang, event ID tetap lebih baik.
Checklist implementasi
- Tentukan kontrak header: signature, timestamp, event ID atau nonce.
- Tandatangani timestamp + raw body, bukan objek JSON hasil parsing.
- Gunakan HMAC dengan algoritme yang jelas dan comparison constant-time.
- Validasi timestamp dengan toleransi waktu yang realistis.
- Sinkronkan jam server dengan NTP atau mekanisme setara.
- Simpan deduplikasi berbasis event ID jika tersedia; gunakan nonce atau hash request jika tidak ada.
- Pastikan duplicate yang sah direspons idempotent, biasanya tetap 200 jika sudah pernah diproses.
- Jangan proses payload sebelum signature dan timestamp lolos.
- Jika memungkinkan, gunakan inbox pattern untuk memisahkan verifikasi dari proses bisnis.
- Rotasi secret dengan masa transisi secret aktif dan secret lama.
- Pastikan proxy dan middleware tidak mengubah body atau menghapus header penting.
- Log hasil verifikasi dan clock skew tanpa membocorkan secret atau payload sensitif.
- Siapkan metrik untuk invalid signature, stale timestamp, duplicate, dan processing latency.
Penutup
Desain endpoint webhook yang tahan replay tidak cukup dengan satu lapisan verifikasi. Signature HMAC membuktikan request asli dan utuh, timestamp membatasi umur request, dan event ID/nonce/hash mencegah pemrosesan ulang di dalam window yang masih valid. Kunci implementasi yang benar ada pada detail: verifikasi terhadap raw body, urutan pemeriksaan yang disiplin, respons idempotent untuk retry sah, dan observability yang memadai.
Jika Anda harus memilih prioritas implementasi, mulailah dari tiga hal ini: verifikasi HMAC pada raw body, validasi timestamp dengan toleransi yang masuk akal, dan deduplikasi berbasis event ID. Kombinasi ini sudah menutup sebagian besar masalah nyata pada integrasi webhook produksi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!