Mengamankan jalur pembayaran
Jalur pembayaran punya kebutuhan keamanan yang berbeda dari sisa portalmu — dan sebagian aturan yang melindungi halaman justru merusaknya.
Intisari
- Jalur webhook harus dikecualikan dari managed WAF rules, rate limiting, dan Bot Fight Mode.
- Server key hanya boleh dibaca kode server. Satu impor yang salah membawanya ke bundel klien.
- Kunci sandbox dan produksi tidak boleh hidup di tempat yang sama.
- Jangan pernah mencatat badan notifikasi lengkap ke log biasa —
signature_keysetara kredensial. - Batasi akses ke endpoint webhook ke rentang IP Midtrans kalau bisa; kalau tidak, tanda tangan yang jadi satu-satunya pertahanan.
WAF yang memblokir uangmu
Managed WAF rules Cloudflare memeriksa badan request untuk pola serangan. Notifikasi Midtrans berisi JSON
dengan field bernama seperti signature_key dan nilai heksadesimal panjang — dan sebagian
aturan generik menganggapnya mencurigakan.
Ekspresi aturan skip di Cloudflare:
(http.request.uri.path eq "/api/midtrans-webhook" and http.request.method eq "POST")
Aksi: Skip
☑ All managed rules
☑ Rate limiting rules
☑ Bot Fight Mode / Super Bot Fight Mode
☑ Browser Integrity Check
Gejalanya sangat menyesatkan. Sebagian besar webhook lewat dengan baik; hanya sebagian kecil yang diblokir — biasanya yang metode pembayarannya menghasilkan field tertentu. Hasilnya: beberapa pelanggan tidak pernah aktif, tanpa pola yang jelas, dan tidak ada apa pun di log aplikasimu karena request-nya tidak pernah sampai. Kamu hanya akan menemukannya di log Cloudflare — kalau kamu tahu harus mencarinya di sana.
Rate limiting punya masalah yang sama tapi lebih tajam: notifikasi datang dari sekumpulan IP Midtrans, dan saat ada lonjakan pembayaran semuanya berasal dari IP yang sama. Bagi aturan rate limit, itu terlihat persis seperti serangan.
Membatasi ke IP Midtrans
(http.request.uri.path eq "/api/midtrans-webhook"
and not ip.src in {a.b.c.d/32 e.f.g.h/32})
Aksi: Block
Ambil daftar IP-nya dari dokumentasi atau dukungan Midtrans, dan perlakukan sebagai lapisan tambahan — bukan pengganti tanda tangan. Daftar IP bisa berubah tanpa pemberitahuan yang kamu baca tepat waktu, dan kalau kamu bergantung padanya sepenuhnya, pembayaranmu berhenti pada hari mereka menambah server baru. Verifikasi tanda tangan tetap yang menjaga kebenarannya; pembatasan IP hanya mengurangi kebisingan.
Kunci yang tidak boleh bocor
| Kunci | Boleh di klien? | Kalau bocor |
|---|---|---|
| Client key | Ya | Tidak apa-apa — memang untuk itu |
| Server key | Tidak pernah | Siapa pun bisa membuat transaksi, refund, dan memalsukan tanda tangan webhook |
ASTRO_KEY | Tidak | Props server island bisa dipalsukan |
| Kredensial database | Tidak | Seluruh data |
// src/lib/env.ts — hanya server
export const env = SkemaEnv.parse(process.env);
// BAHAYA: satu impor di komponen Vue membawa seluruh modul ke bundel klien
import { env } from "@/lib/env"; // ← di dalam .vue
// Yang benar: kirim hanya yang publik sebagai props dari Astro.
<TombolLangganan client:visible clientKey={env.MIDTRANS_CLIENT_KEY} />
Vite akan memperingatkanmu untuk sebagian kasus, tapi tidak semuanya. Pagar yang lebih andal: jalankan pencarian di hasil build sebagai bagian dari CI.
# Gagalkan build kalau server key muncul di bundel klien
pnpm build
if grep -rl "Mid-server-\|SB-Mid-server-" dist/client/ 2>/dev/null | grep -q .; then
echo "FATAL: server key bocor ke bundel klien"
exit 1
fi
Pisahkan sandbox dan produksi
| Praktik | Kenapa |
|---|---|
| Secret berbeda di Secrets Manager | Tidak mungkin tertukar lewat variabel lingkungan |
| Validasi awalan kunci saat start | startsWith("Mid-server-") untuk produksi |
| Database berbeda | Transaksi uji tidak mengotori laporan keuangan |
| URL webhook berbeda | Notifikasi sandbox tidak pernah mencapai produksi |
const SkemaEnv = z.object({
MIDTRANS_PRODUKSI: z.coerce.boolean(),
MIDTRANS_SERVER_KEY: z.string(),
MIDTRANS_CLIENT_KEY: z.string(),
}).refine(
(v) => v.MIDTRANS_PRODUKSI
? v.MIDTRANS_SERVER_KEY.startsWith("Mid-server-")
: v.MIDTRANS_SERVER_KEY.startsWith("SB-Mid-server-"),
{ message: "Kunci Midtrans tidak cocok dengan mode produksi/sandbox" },
);
Container dengan kunci yang salah gagal start, dan ECS membatalkan deploy. Alternatifnya: portal produksi diam-diam memakai kunci sandbox, semua pembayaran "berhasil", dan tidak ada uang yang masuk selama berjam-jam sampai ada yang memeriksa dashboard.
Apa yang boleh masuk log
// BURUK
console.log("notifikasi:", JSON.stringify(n));
// BAIK — hanya yang dibutuhkan untuk menelusuri masalah
console.log(JSON.stringify({
level: "info",
peristiwa: "webhook-diterima",
order_id: n.order_id,
status: n.transaction_status,
payment_type: n.payment_type,
// signature_key, saved_token, dan detail kartu TIDAK ikut
}));
| Field | Log biasa? |
|---|---|
order_id, transaction_id, transaction_status | Ya |
gross_amount, payment_type | Ya |
signature_key | Tidak — setara kredensial |
saved_token_id | Tidak — bisa dipakai menagih |
masked_card | Boleh (sudah tersamar), tapi tidak perlu |
| Email & nama pembeli | Hindari; pakai ID member |
Badan notifikasi lengkap tetap disimpan di tabel notifikasi_pembayaran, yang aksesnya
terbatas. Bedanya dengan log aplikasi: log mengalir ke CloudWatch, sering bisa dibaca lebih banyak orang,
dan tersimpan di tempat yang tidak masuk hitungan keamanan datamu.
Daftar periksa
| Item | Verifikasi dengan |
|---|---|
| Webhook dikecualikan dari WAF & rate limit | Kirim 50 POST beruntun; semuanya harus sampai |
| Server key tidak ada di bundel klien | grep di dist/client/ |
| Tanda tangan diverifikasi waktu-tetap | Tinjau kode |
| Kunci produksi punya awalan yang benar | Validasi Zod saat start |
| Log tidak berisi tanda tangan atau token | Cari di CloudWatch |
| Harga selalu dari database | Coba kirim gross_amount dari klien |
| Endpoint refund butuh peran admin | Panggil sebagai member biasa |
Latihan: pasang aturan skip WAF untuk jalur webhook di Cloudflare, lalu verifikasi dengan
mengirim 50 POST beruntun dari satu IP — semuanya harus mencapai origin. Tambahkan pemeriksaan
grep ke skrip build-mu dan buktikan ia gagal: impor env ke sebuah komponen
.vue, build, dan pastikan skripnya menangkapnya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.