Webhook sering menjadi bagian yang tidak terlihat dari sebuah website. Ketika pembayaran berhasil, pesanan berubah status, atau ada commit baru di repositori, layanan lain mengirim HTTP POST ke URL aplikasi kita. Praktis, tetapi ada satu masalah mendasar: endpoint webhook pada dasarnya adalah pintu publik yang menunggu request dari internet.
Jika setiap request langsung dipercaya, siapa pun dapat mengirim payload palsu seolah-olah berasal dari penyedia layanan. Akibatnya bisa serius: status pesanan berubah tanpa pembayaran, akun mendapat hak akses yang keliru, atau notifikasi terkirim berulang kali.
Karena itu, webhook sebaiknya diperlakukan seperti API khusus: harus memeriksa keaslian request, membatasi bentuk data, dan memastikan event yang sama tidak diproses dua kali.
Webhook bukan sekadar URL penerima
Dalam alur sederhana, penyedia layanan mengirim payload JSON ke endpoint seperti https://contoh.com/webhook/payment. Server kemudian membaca data tersebut dan menjalankan aksi tertentu.
Masalahnya, alamat URL saja tidak membuktikan siapa pengirimnya. HTTPS melindungi data saat transit, tetapi aplikasi tetap perlu memeriksa bahwa request memang dibuat oleh layanan yang dipercaya. GitHub, misalnya, menyediakan secret webhook dan tanda tangan HMAC pada header X-Hub-Signature-256 untuk membantu penerima memvalidasi payload. Dokumentasi GitHub juga menekankan agar signature diverifikasi sebelum payload diproses lebih lanjut.
Urutan pemeriksaan yang lebih aman
Jangan langsung memanggil json_decode() lalu memperbarui database. Gunakan urutan berikut:
- Pastikan metode request adalah
POST. - Baca raw request body sebelum mengubahnya menjadi array PHP.
- Ambil signature dari header.
- Hitung signature yang diharapkan menggunakan secret di server.
- Bandingkan signature dengan fungsi perbandingan konstan waktu.
- Validasi ukuran dan struktur payload.
- Periksa apakah event sudah pernah diproses.
- Simpan atau masukkan event ke antrean sebelum mengembalikan respons sukses.
Raw body penting karena signature dihitung dari isi request asli. Jika JSON di-decode lalu disusun ulang, perubahan spasi, urutan properti, atau format karakter dapat membuat hasil hash berbeda.
Contoh validasi signature HMAC di PHP
Contoh berikut mengikuti pola signature GitHub, yaitu prefix sha256= diikuti hasil HMAC-SHA256 dari raw body. Format header setiap penyedia bisa berbeda, jadi jangan menganggap semua webhook memakai nama header atau skema yang sama.
<?php
$secret = $_ENV['WEBHOOK_SECRET'] ?? '';
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method not allowed');
}
if ($secret === '' || $rawBody === '' || $signature === '') {
http_response_code(401);
exit('Unauthorized');
}
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Unauthorized');
}
$data = json_decode($rawBody, true);
if (!is_array($data)) {
http_response_code(400);
exit('Invalid payload');
}
// Lanjutkan dengan validasi event dan pemrosesan.
http_response_code(200);
echo 'OK';Fungsi hash_equals() dipakai agar perbandingan tidak menggunakan operator == biasa. GitHub merekomendasikan perbandingan konstan waktu untuk mengurangi risiko kebocoran informasi melalui perbedaan waktu respons. Secret juga sebaiknya disimpan di environment variable atau secret manager, bukan ditulis langsung di repository.
Signature belum menyelesaikan serangan replay
Signature yang valid belum tentu berarti request baru. Jika penyerang berhasil merekam satu request yang sah, request tersebut dapat dikirim ulang. Ini disebut replay attack.
Perlindungannya bergantung pada kemampuan penyedia webhook. Jika payload atau header menyertakan timestamp dan event ID yang ikut ditandatangani, server dapat menolak request yang terlalu lama dan mencatat ID yang sudah diterima. OWASP merekomendasikan kombinasi timestamp bertoleransi singkat dan deduplikasi berdasarkan event ID bila protokol penyedia mendukungnya.
Untuk layanan yang hanya menyediakan signature atas raw body, seperti pola dasar GitHub, jangan mengarang mekanisme timestamp sendiri lalu menganggapnya otomatis aman. Gunakan ID delivery yang disediakan untuk mendeteksi pengiriman ulang dan buat proses bisnis tetap idempoten.
Cegah pembayaran atau aksi ganda dengan idempotensi
Idempotensi berarti request yang sama dapat diterima berulang kali tanpa menggandakan efek akhirnya. Ini penting karena pengirim webhook dapat mencoba mengirim ulang ketika respons sebelumnya terlambat atau gagal diterima.
Misalnya, sebelum memproses event pembayaran, simpan ID event dalam tabel khusus:
CREATE TABLE webhook_events (
event_id VARCHAR(191) PRIMARY KEY,
event_type VARCHAR(100) NOT NULL,
received_at DATETIME NOT NULL,
processed_at DATETIME NULL
);Alurnya adalah:
- Verifikasi signature.
- Ambil
event_iddari payload atau header. - Coba masukkan ID tersebut ke tabel.
- Jika terkena duplicate key, anggap event sudah pernah diproses.
- Jika berhasil, jalankan aksi bisnis dalam transaksi yang sesuai.
Jangan hanya mengandalkan pengecekan SELECT lalu INSERT terpisah. Dua request yang datang hampir bersamaan dapat sama-sama lolos pengecekan. Primary key atau unique index di database memberi perlindungan yang lebih kuat.
Jangan memproses semuanya di dalam request
Webhook sebaiknya merespons cepat. Jika handler langsung mengirim email, memperbarui banyak tabel, atau memanggil beberapa API lain, request bisa timeout. Pengirim kemudian mengira pengiriman gagal dan mencoba lagi, sehingga duplikasi makin mungkin terjadi.
Pola yang lebih stabil adalah memverifikasi request, menyimpan event ke database atau antrean, lalu mengembalikan respons sukses setelah data benar-benar tersimpan. Worker terpisah dapat memproses pekerjaan berat di belakang layar. Panduan GitHub tentang webhook juga menyarankan pemrosesan di latar belakang agar penerimaan delivery tidak terhambat pekerjaan panjang.
Checklist sebelum webhook dipakai di produksi
- Gunakan HTTPS dan sertifikat yang valid.
- Simpan secret di environment variable, bukan di source code.
- Verifikasi signature dari raw body.
- Gunakan
hash_equals()atau fungsi perbandingan konstan waktu yang setara. - Tolak method selain
POST. - Batasi ukuran body request.
- Validasi event type, ID, dan field penting sebelum menyentuh database.
- Simpan event ID untuk mencegah pemrosesan ganda.
- Jangan mencatat secret, signature, atau seluruh payload sensitif ke log.
- Siapkan alarm untuk lonjakan signature gagal, respons
4xx, dan antrean yang menumpuk.
Apa artinya bagi kita?
Webhook yang aman bukan ditentukan oleh satu baris HMAC saja. Keamanan muncul dari beberapa lapisan: signature membuktikan keaslian dan integritas payload, timestamp atau deduplikasi membantu menghadapi replay, database mencegah efek ganda, sedangkan antrean menjaga endpoint tetap responsif.
Mulailah dari webhook yang paling berdampak—misalnya pembayaran, perubahan hak akses, dan pengiriman barang. Setelah itu, terapkan pola yang sama ke integrasi lain. Sedikit disiplin di awal jauh lebih murah daripada memperbaiki pesanan palsu atau transaksi ganda setelah aplikasi sudah berjalan.
Sumber & bacaan lebih lanjut
- OWASP Webhook Security Cheat Sheet
- GitHub Docs: Validating webhook deliveries
- GitHub Docs: Best practices for using webhooks
– Rio Yotto @rioyotto
