Membangun platform pembayaran dan mini-ERP yang digunakan oleh banyak aplikasi partner (multi-tenant partner ecosystem) menuntut standar keamanan dan keandalan operasional yang tinggi. Setiap titik masuk (inbound) dan titik keluar (outbound) data harus dilindungi dari manipulasi, kebocoran kredensial, hingga kegagalan pengiriman notifikasi.
Dalam artikel kelima sekaligus penutup dari seri portofolio engineering ini, saya akan membedah empat komponen arsitektur keamanan dan operasional yang saya implementasikan di proyek ini:
Pada fase awal sistem, autentikasi antar-layanan menggunakan satu kunci rahasia statis (single shared secret) di environment variable dan mengirim waktu request di dalam body (body.request_time).
Namun, ketika jumlah aplikasi partner (Integration Apps) bertambah, pendekatan satu kunci statis tidak lagi aman: jika satu aplikasi bocor, seluruh ekosistem terancam, dan kita tidak bisa membatasi aplikasi A hanya boleh mengakses data Partner A.
Tantangannya: Bagaimana cara menerapkan skema Public/Private Key per aplikasi partner tanpa mematikan aplikasi klien lama yang sudah berjalan di produksi?
IntegrationApp Architecture, Clock-Skew Tolerance, & Backward-Compatible GuardSaya merancang mekanisme kredensial multi-aplikasi di mana setiap aplikasi klien memiliki pasangan public_key (dikirim melalui header x-app-key) dan private_key (disimpan terenkripsi AES di database, hanya ditampilkan satu kali saat pembuatan/regenerasi).
x-timestamp dalam format ISO-8601 UTC (Z).x-timestamp. Jika selisihnya lebih dari 5 menit, request langsung ditolak untuk mencegah penyerang menangkap dan mengirim ulang paket yang sama (replay attack), sekaligus tetap memberi toleransi wajar jika jam server partner sedikit bergeser (clock skew).x-app-key dikirim, Guard memvalidasi signature menggunakan private_key milik aplikasi partner dari database.x-app-key belum dikirim (klien lama yang belum update), Guard otomatis melakukan fallback menggunakan secret lama dan membaca body.request_time. Seluruh klien lama tetap berjalan tanpa downtime selama masa transisi!null).Ketika transaksi pembayaran telah berhasil (SUCCESS), sistem mengirimkan webhook callback ke URL milik partner. Namun, sering kali server partner mengalami downtime atau maintenance sehingga mereka meminta pengiriman ulang callback (Resend Callback).
Jika fitur Resend Callback hanya sekadar mengirim ulang JSON mentah dari log lama:
invoice: null yang merusak proses pembukuan di sisi mereka.Saya membangun layanan pengiriman ulang callback yang memvalidasi kelengkapan hulu dan merakit ulang tanda tangan kriptografis secara segar:
// Prototype Konseptual: Smart Resend Callback dengan Validasi Invoice ERP & Re-Signing
async function resendPartnerWebhook(identifier: string) {
const tx = await db.transactions.findByAnyIdentifier(identifier);
// 1. Validasi Ketat Prasyarat Bisnis & Kesiapan Invoice Core ERP
if (!tx || tx.status !== 'SUCCESS') {
throw new BadRequestError(
'Hanya transaksi berstatus SUCCESS yang dapat dikirim ulang',
);
}
if (tx.requiresErpInvoice && !tx.invoice?.invoiceNumber) {
throw new BadRequestError(
'Invoice Core ERP belum terbentuk, pengiriman callback ditahan',
);
}
// 2. Rakit payload dengan timestamp baru & tanda tangani ulang (Fresh HMAC Signature)
const freshPayload = {
requestTime: new Date().toISOString(),
orderCode: tx.orderCode,
status: tx.status,
amount: Number(tx.amount),
invoiceNo: tx.invoice?.invoiceNumber ?? null,
};
const partnerSecret = await secretManager.resolveDecryptedSecret(
tx.partnerAppId,
);
const freshSignature = createHmacSha256(freshPayload, partnerSecret);
// 3. Kirim ke URL partner & catat hasil respons ke Audit Log
const response = await httpClient.post(tx.callbackUrl, freshPayload, {
headers: { 'x-signature': freshSignature },
});
await auditLogger.recordCallbackAttempt(tx.id, freshPayload, response);
return response;
}
Pada modul impor data pelanggan massal, staf admin dapat mengunggah file Excel (.xlsx) berisi ribuan baris data pelanggan untuk diimpor ke database lokal maupun Core ERP.
Jika di dalam file Excel terdapat sel nama atau alamat yang diawali karakter eksekusi formula seperti =HYPERLINK(...), @SUM(...), atau +cmd|..., ketika data tersebut disimpan lalu diekspor kembali ke dalam laporan Excel oleh tim Finance, aplikasi spreadsheet dapat mengeksekusi payload berbahaya tersebut secara otomatis!
Tantangannya adalah: kita harus membersihkan karakter + atau - yang berbahaya (seperti +cmd...), tetapi tidak boleh merusak nomor telepon internasional atau angka negatif yang valid (seperti +62812345678 atau -5000)!
Untuk itu, saya merancang fungsi sanitasi berbasis Regular Expression dengan Negative Lookahead ((?!\d)):
// Prototype Konseptual: Sanitasi Sel Excel dengan Negative Lookahead Regex
function sanitizeSpreadsheetCell(value: unknown): string {
if (value === null || value === undefined) return '';
const text = String(value).trim();
// Deteksi jika diawali '=' atau '@', ATAU diawali '+' / '-' yang TIDAK diikuti angka
if (/^[=@]/.test(text) || /^[-+](?!\d)/.test(text)) {
return text.replace(/^[=@+-]+/, '').trim();
}
return text;
}
Dengan pola /^[-+](?!\d)/:
"+cmd|'/C calc'" $\rightarrow$ terdeteksi karena karakter setelah + adalah huruf c (bukan digit angka), sehingga tanda + otomatis dibersihkan."+62812345678" $\rightarrow$ aman dan tidak terpotong karena karakter setelah + adalah digit angka 6!Untuk mempermudah proses debugging dan audit keamanan, seluruh lalu lintas HTTP request dan response dicatat secara otomatis oleh Global Logging Interceptor.
Agar pencatatan log otomatis ini tidak menimbulkan kebocoran kredensial maupun pembengkakan ukuran database (database bloat), saya menerapkan dua mekanisme di dalam interceptor:
password, secret, privateKey, token) secara rekursif sebelum objek log disimpan.total_items) beserta 2 sampel elemen pertama (sample_items):// Prototype Konseptual: Deep Sanitization & Smart Array Truncation pada Log Interceptor
function prepareResponseForLog(rawResponse: unknown) {
const masked = deepMaskSensitiveKeys(rawResponse);
const serialized = JSON.stringify(masked);
// Jika ukuran payload melebihi batas aman log, pangkas secara cerdas
if (serialized.length > MAX_LOG_CHARS && Array.isArray(masked)) {
return {
_truncated: true,
totalItems: masked.length,
sampleItems: masked.slice(0, 2), // Simpan 2 sampel pertama saja untuk hemat storage
};
}
return masked;
}
x-app-key + x-timestamp) tanpa menyebabkan downtime sedikit pun./^[-+](?!\d)/) menutup celah Excel Formula Injection tanpa merusak format nomor telepon (+62...) milik pelanggan.