← Semua pembelajaran / Astro Nol → Portal Berita
Fase 6 · Midtrans & Langganan

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_key setara 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

KunciBoleh di klien?Kalau bocor
Client keyYaTidak apa-apa — memang untuk itu
Server keyTidak pernahSiapa pun bisa membuat transaksi, refund, dan memalsukan tanda tangan webhook
ASTRO_KEYTidakProps server island bisa dipalsukan
Kredensial databaseTidakSeluruh 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

PraktikKenapa
Secret berbeda di Secrets ManagerTidak mungkin tertukar lewat variabel lingkungan
Validasi awalan kunci saat startstartsWith("Mid-server-") untuk produksi
Database berbedaTransaksi uji tidak mengotori laporan keuangan
URL webhook berbedaNotifikasi 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
}));
FieldLog biasa?
order_id, transaction_id, transaction_statusYa
gross_amount, payment_typeYa
signature_keyTidak — setara kredensial
saved_token_idTidak — bisa dipakai menagih
masked_cardBoleh (sudah tersamar), tapi tidak perlu
Email & nama pembeliHindari; 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

ItemVerifikasi dengan
Webhook dikecualikan dari WAF & rate limitKirim 50 POST beruntun; semuanya harus sampai
Server key tidak ada di bundel kliengrep di dist/client/
Tanda tangan diverifikasi waktu-tetapTinjau kode
Kunci produksi punya awalan yang benarValidasi Zod saat start
Log tidak berisi tanda tangan atau tokenCari di CloudWatch
Harga selalu dari databaseCoba kirim gross_amount dari klien
Endpoint refund butuh peran adminPanggil 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.