Webhook adalah callback HTTP, yaitu panggilan yang dilakukan satu sistem ke URL sistem lain ketika suatu peristiwa terjadi, misalnya pemberitahuan bahwa peminjaman buku telah disetujui. Karena siapa pun yang mengetahui URL dapat mengirimkan permintaan serupa, penerima tidak boleh menilai keabsahan permintaan dari URL atau alamat IP-nya semata. Topik ini membahas cara menandatangani permintaan yang dikirim sendiri dan memverifikasi permintaan dari pihak lain menggunakan HMAC-SHA256 atas pesan timestamp.body, disertai jendela toleransi sebagai penghalau pemutaran ulang (replay). Seluruh kasus dieksekusi dengan pustaka standar Python pada nilai yang tetap, sehingga keluarannya dapat direproduksi.
Mengapa webhook perlu tanda tangan#
TLS menjaga kerahasiaan dan integritas saluran, tetapi saluran yang aman tidak membuktikan bahwa pengirimnya berhak mengirim peristiwa tersebut. Kebutuhan itu dipenuhi dengan secret bersama, yaitu nilai rahasia yang diketahui kedua sistem dan dipertukarkan di luar jalur permintaan, misalnya pada saat konfigurasi endpoint. Pengirim menghitung tanda tangan atas pesan timestamp + "." + body menggunakan HMAC-SHA256, lalu mengirimkan body beserta header X-Webhook-Timestamp dan X-Webhook-Signature. Penerima menghitung ulang tanda tangan atas body yang diterima, kemudian membandingkannya dengan tanda tangan pada header.
Timestamp dipasangkan dengan body dalam satu pesan supaya tanda tangan yang sah tidak dapat digunakan ulang tanpa batas waktu. Tanpa penanda waktu, satu permintaan yang terintersepsi dapat dikirim ulang selamanya, dan penerima tidak memiliki dasar untuk menolaknya. Dengan jendela toleransi, permintaan yang sama hanya sah selama beberapa menit sejak waktu pengirimannya. Kegagalan pada verifikasi semacam ini termuat dalam kategori A08:2025 Software or Data Integrity Failures pada OWASP Top 10:2025, yaitu kegagalan memastikan bahwa data yang diproses berasal dari sumber yang sah dan tidak berubah di tengah jalan.
Menandatangani dan memverifikasi dengan HMAC-SHA256#
Skrip berikut memuat pengirim dan penerima webhook pada satu berkas. Nilai secret sengaja dibuat jelas fiktif, sedangkan seluruh timestamp ditetapkan tetap, sehingga keluarannya dapat direproduksi dan dibandingkan. Empat kasus diuji secara berurutan, yaitu payload asli, body yang diubah, tanda tangan yang dihitung dengan secret salah, dan pengiriman ulang setelah satu jam. Jalankan dengan perintah python3 webhook-signature.py.
"""Verifikasi tanda tangan webhook HMAC-SHA256.
Pengirim menandatangani pesan "timestamp.body" dengan secret bersama.
Penerima menghitung ulang tanda tangan, membandingkannya dengan
hmac.compare_digest, lalu menolak body yang diubah, secret yang
salah, dan timestamp di luar jendela toleransi. Semua timestamp
ditetapkan tetap agar keluarannya dapat direproduksi.
"""
import hashlib
import hmac
import json
SECRET_PENERIMA = b"secret-bersama-fiktif-perpustakaan-2026"
SECRET_PENGIRIM_SALAH = b"secret-salah-yang-juga-fiktif"
TOLERANSI_DETIK = 300
WAKTU_KIRIM = 1700000000
def buat_tanda_tangan(secret, timestamp, body):
pesan = f"{timestamp}.".encode() + body
return hmac.new(secret, pesan, hashlib.sha256).hexdigest()
def kirim_webhook(secret, timestamp, body):
return {
"X-Webhook-Timestamp": str(timestamp),
"X-Webhook-Signature": buat_tanda_tangan(secret, timestamp, body),
}
def verifikasi_webhook(headers, body, secret, waktu_verifikasi):
timestamp = headers.get("X-Webhook-Timestamp")
signature = headers.get("X-Webhook-Signature")
if timestamp is None or signature is None:
return "ditolak: header tanda tangan tidak lengkap"
try:
waktu_kirim = int(timestamp)
except ValueError:
return "ditolak: timestamp bukan bilangan bulat"
if abs(waktu_verifikasi - waktu_kirim) > TOLERANSI_DETIK:
return "ditolak: timestamp di luar jendela toleransi"
tanda_hitung = buat_tanda_tangan(secret, waktu_kirim, body)
if not hmac.compare_digest(tanda_hitung, signature):
return "ditolak: tanda tangan tidak cocok"
return "diterima"
body_asli = json.dumps(
{"peristiwa": "peminjaman.disetujui", "id": 24}, separators=(",", ":")
).encode()
print("== Empat kasus verifikasi webhook (toleransi 300 detik) ==")
header_valid = kirim_webhook(SECRET_PENERIMA, WAKTU_KIRIM, body_asli)
print("1. payload asli:", verifikasi_webhook(
header_valid, body_asli, SECRET_PENERIMA, WAKTU_KIRIM + 100))
print(" tanda tangan :", header_valid["X-Webhook-Signature"])
body_diubah = json.dumps(
{"peristiwa": "peminjaman.disetujui", "id": 25}, separators=(",", ":")
).encode()
print("2. body diubah :", verifikasi_webhook(
header_valid, body_diubah, SECRET_PENERIMA, WAKTU_KIRIM + 100))
header_secret_salah = kirim_webhook(SECRET_PENGIRIM_SALAH, WAKTU_KIRIM, body_asli)
print("3. secret salah:", verifikasi_webhook(
header_secret_salah, body_asli, SECRET_PENERIMA, WAKTU_KIRIM + 100))
header_replay = kirim_webhook(SECRET_PENERIMA, WAKTU_KIRIM, body_asli)
print("4. replay 1 jam:", verifikasi_webhook(
header_replay, body_asli, SECRET_PENERIMA, WAKTU_KIRIM + 3600))== Empat kasus verifikasi webhook (toleransi 300 detik) ==
1. payload asli: diterima
tanda tangan : 6494945e6089ad92fba158782e26fd6423ba914c763de56cb94d774c77e06c7f
2. body diubah : ditolak: tanda tangan tidak cocok
3. secret salah: ditolak: tanda tangan tidak cocok
4. replay 1 jam: ditolak: timestamp di luar jendela toleransiKasus pertama diterima karena tanda tangan yang dihitung ulang atas body asli sama dengan tanda tangan pada header, sedangkan jarak waktunya 100 detik masih dalam toleransi 300 detik. Kasus kedua mengubah nilai id dari 24 menjadi 25 tanpa menghitung ulang tanda tangan, sehingga hasil hitung ulang berbeda dari header yang dikirim. Kasus ketiga memperlihatkan peran secret pada kunci verifikasi, karena tanda tangan yang dibuat dengan secret lain tidak pernah cocok meskipun bodynya utuh.
Perbandingan tanda tangan dilakukan dengan hmac.compare_digest, bukan operator ==. Perbandingan biasa berhenti pada byte pertama yang berbeda, sehingga waktu respons berbeda antara tanda tangan yang mirip di awal dan tanda tangan yang berbeda sejak awal. Perbedaan terukur ini dapat digunakan untuk merekonstruksi tanda tangan byte demi byte. Serangan yang bertumpu pada perbedaan waktu respons disebut timing attack. Fungsi compare_digest membandingkan seluruh nilai dalam waktu yang tidak bergantung pada posisi perbedaannya, sehingga tidak ada posisi yang bocor.
Jendela toleransi dan kegagalan fail-closed#
Jendela toleransi mengakomodasi perbedaan jam antar sistem, karena dua server jarang memiliki waktu yang persis sama. Jendela yang terlalu sempit menolak permintaan yang sah, sedangkan jendela yang terlalu lebar memperpanjang masa pakai permintaan yang terintersepsi. Nilai 300 detik lazim digunakan karena memberi kelonggaran tanpa terlalu lama, dan nilainya ditetapkan sebagai konfigurasi agar dapat disesuaikan. Kasus keempat pada keluaran menunjukkan pengiriman ulang setelah satu jam ditolak semata karena waktunya, meskipun tanda tangannya sah.
Sikap penerima harus fail-closed, yaitu menolak permintaan ketika pemeriksaan tidak dapat diselesaikan, misalnya header yang tidak lengkap atau timestamp yang tidak dapat diurai. Kebalikannya, fail-open, meloloskan permintaan ketika verifikasi gagal dan mengubah seluruh mekanisme ini menjadi tempelan. Untuk peristiwa yang tidak boleh diproses dua kali, jendela waktu dipadukan dengan catatan pengenal peristiwa, sehingga permintaan dengan id yang sama dalam jendela hanya diproses sekali. Permintaan yang ditolak dicatat beserta alasannya agar pola penyusupan dapat memunculkan peringatan, sedangkan diagram berikut merangkum alur penandatanganan dari sisi pengirim sampai keputusan penerima.
Kesalahan umum#
Kesalahan berikut sering muncul pada endpoint webhook yang baru dibangun.
- Memeriksa keberadaan header tanda tangan tanpa menghitung ulang nilainya, sehingga nilai apa pun lolos.
- Menyimpan secret pada kode sumber atau repositori, padahal secret dibagikan melalui konfigurasi yang dijaga.
- Melewatkan pemeriksaan timestamp, sehingga permintaan yang terintersepsi dapat dikirim ulang tanpa batas waktu.
- Menangkap pengecualian verifikasi lalu melanjutkan pemrosesan, yang mengubah penerima menjadi fail-open.
Ringkasan#
- Webhook diverifikasi dengan HMAC-SHA256 atas pesan
timestamp.bodymenggunakan secret bersama, dan penerima menghitung ulang tanda tangan tersebut sebelum memproses body. - Perbandingan menggunakan
hmac.compare_digest, sedangkan jendela toleransi pada timestamp membatasi masa pakai permintaan untuk menghalau replay. - Penerima bersikap fail-closed, yaitu menolak permintaan ketika pemeriksaan tidak dapat diselesaikan, dan mencatat setiap penolakan beserta alasannya.
Referensi#
- A08:2025 Software or Data Integrity Failures — OWASP Top 10 — Kategori OWASP Top 10:2025 yang menaungi kegagalan memastikan data berasal dari sumber yang sah dan tidak berubah.
- hmac — Keyed-Hashing for Message Authentication — Python documentation
— Dokumentasi pustaka standar Python untuk HMAC beserta fungsi perbandingan konstan
compare_digest. - Cross-Site Request Forgery Prevention Cheat Sheet — OWASP Cheat Sheet Series — Panduan OWASP tentang memverifikasi permintaan yang datang dari pihak lain, termasuk token dan pemeriksaan asal.