Keamanan Webhook: Verifikasi Signature dan Perlindungan Replay
Penerima webhook harus memastikan delivery berasal dari pengirim yang diharapkan dan masih aman diproses. TLS melindungi transport, bukan request bertanda tangan yang diputar ulang. RFC 9421 mendefinisikan HTTP message signatures; skema tiap provider tetap memerlukan verifikasi raw body.
Verifikasi byte sebelum parsing
Hitung MAC atau verifikasi signature atas byte request asli, header terpilih, timestamp, dan key identifier sebelum JSON diparsing. Framework yang melakukan serialisasi ulang JSON dapat mengubah whitespace dan merusak representasi bertanda tangan. Contoh: event.created diterima sekali lalu event ID yang sama ditolak saat retry. Contoh lain: signature valid pada event payout lama tidak boleh memicu kredit kedua.
Worksheet penerima
| Kontrol | Uji | Bukti yang diharapkan |
|---|---|---|
| signature | satu byte diubah | ditolak sebelum business handler |
| freshness | timestamp di luar jendela | tidak ada pesan queue |
| replay | event ID diulang | hasil idempoten |
| rotasi key | key lama dan baru | overlap terbatas |
CWE-294 membahas kelemahan replay; RFC 9421 menjelaskan komponen pesan bertanda tangan.
Aturan keputusan: jangan memasukkan pesan ke queue atau mengubah state sampai autentisitas, freshness, dan idempotensi lolos.
Urutan perbaikan
Pertahankan raw body, pakai perbandingan constant-time, simpan kunci deduplikasi secara atomik bersama side effect, lalu uji clock skew serta retry provider. Kontrak pengirim menentukan header dan kebijakan retry yang tepat; artikel ini tidak menggantikannya. Audit kode dapat menelusuri batas handler.
Bangun batas penerimaan
Penerima perlu menyimpan byte mentah sebelum body parser mengubahnya. Pilih hanya header, algoritme, dan cara kanonik yang didokumentasikan provider, ikat timestamp atau masa berlaku pada delivery, lalu cari key aktif melalui key identifier eksplisit. Tolak metadata yang rusak sebelum kerja kriptografi. Bandingkan nilai MAC secara constant-time. Signature membuktikan integritas representasi yang dicakup; signature tidak menentukan transisi bisnis mana yang sah.
Handler praktis memakai satu transaksi database: cadangkan gabungan provider dan event ID dengan unique constraint, buat perubahan state yang dimaksud, lalu commit sekali. Jika cadangan duplikat kalah, kembalikan sukses yang kompatibel dengan provider tanpa mengulang side effect. Simpan waktu penerimaan, ID event provider, ID key signature, hasil freshness, dan hasil handler. Jangan mencatat isi body yang rahasia. Bila memakai queue, masukkan pesan hanya setelah penerimaan lulus dan bawa key idempotensi ke consumer.
Bukti dan catatan pengecualian
Bukti rilis memerlukan penolakan body yang diubah, penolakan delivery kedaluwarsa, bukti event duplikat, penerimaan key aktif, perilaku key yang pensiun, serta bukti pemeriksaan gagal tidak membuat state. Owner: pemilik layanan integrasi. Lulus: semua delivery tidak valid berhenti sebelum queue atau mutasi state. Gagal: pemeriksaan gagal mencapai handler. Pengecualian menyebut provider, jenis event terdampak, kontrol kompensasi, pemilik risiko, owner perbaikan, dan tanggal kedaluwarsa.
Contoh request penerima
Profil provider dapat mewajibkan POST ke hook route tetap serta header event ID, timestamp, key ID, dan signature. Simpan byte mentah, bentuk signed input persis seperti profil, lalu verifikasi sebelum parsing. Kembalikan kegagalan generik tanpa mengungkap komponen yang gagal. Saat rotasi, terima key aktif dan key pensiun bernama hanya selama overlap tercatat.
Buku penerimaan webhook
Output yang diharapkan: digest SHA-256 body mentah, ID key, ID event provider, waktu diterima, hasil signature, hasil freshness, outcome transaksi. Bukti ini menghubungkan keputusan signature dengan hasil handler yang tercatat.
Kegagalan teruji: Body yang ditandatangani untuk event A datang memakai ID event B. Verifikasi harus gagal sebelum JSON parsing; event lama yang valid tidak boleh mencadangkan baris bisnis baru.
Kasus tepi: Clock skew dapat sah saat pemulihan provider. Simpan toleransi terdokumentasi, tetapi jangan biarkan toleransi menggantikan keunikan event ID.
Uji penutupan: Putar ulang delivery valid yang sudah dicapture setelah commit pertama. Penutupan lulus hanya bila respons idempoten dan jumlah side effect tetap satu.
Drill siklus hidup delivery
Konfigurasikan pengirim uji dengan dua event ID dan satu objek bisnis. Kirim delivery pertama, ulangi sebelum handler pertama selesai, lalu ulangi setelah commit. Output yang diharapkan adalah satu transisi objek yang committed, satu baris receipt durable, dan acknowledgement aman untuk duplikat. Berikutnya ubah satu byte setelah signing dan pastikan tidak ada parser error, pesan queue, atau business error retryable yang menyamarkan penolakan signature. Uji pensiunnya key dengan fixture identik yang ditandatangani memakai key aktif, key pensiun, dan key ID tidak dikenal. Identifier tidak dikenal harus gagal tanpa mencari key store tanpa batas.
Retensi penting karena deduplikasi tidak dapat berlangsung selamanya tanpa horizon event yang dinyatakan. Turunkan retensi receipt dari jendela retry provider dan risiko pembalikan bisnis, lalu dokumentasikan perilaku setelah kedaluwarsa. Jika event terlambat diizinkan, arahkan ke state review, bukan melewati freshness check secara diam-diam. Artefak akhir adalah test handler yang menunjukkan rollback transaksi ketika reservasi receipt atau mutasi bisnis gagal.
Penerima harus mengikat method dan route target pada profil provider ketika komponen itu penting untuk dispatch. Jika tidak, signature valid untuk satu hook dapat diterima handler lain. Capture route kanonik sebelum middleware menulis ulang. Fixture regresi harus membuktikan request bertanda tangan ke route tetangga tidak dapat mencadangkan event receipt. Bila provider tidak menandatangani informasi route, gunakan secret endpoint atau key penerima terpisah dan dokumentasikan batasnya.