Email Service — Keamanan & Hardening
Semua item di bawah telah diimplementasikan dan diverifikasi per audit keamanan 2026-05-29–2026-06-05.
Proteksi Data & Privasi
Redaksi Payload Sensitif Sebelum Penyimpanan
handleLog memanggil redactSensitiveFields(req.Template, req.Payload) sebelum memanggil store.Insert — memastikan OTP code dan password reset URL tidak pernah tersimpan sebagai plaintext di tabel notification.email_messages. Pendekatan ini konsisten dengan jalur handleSend yang sudah melakukan redaksi lebih awal, sehingga kedua path persistensi terlindungi secara merata.
Keamanan Email & Input Validation
Pencegahan Email Header Injection
sanitizeHeaderValue() helper secara eksplisit mem-strip karakter \r (CR) dan \n (LF) dari semua nilai header email sebelum konstruksi pesan. Strip diterapkan secara defense-in-depth di tiga titik: field From-Name saat NewSender, serta field To dan Subject di buildMultipartAlternativeMessage dan buildPlainTextMessage.
Selain itu, ValidateEmailAddress() menggunakan net/mail.ParseAddress untuk memvalidasi format alamat to_email di handler handleSend — request dengan alamat tidak valid ditolak dengan respons 400 sebelum sampai ke layer dispatch. Kombinasi ini mencegah serangan payload seperti victim@example.com\r\nBcc: attacker@evil.com yang dapat mengubah service menjadi relay spam atau phishing.
Error SMTP Tidak Bocor ke Caller
Error SMTP di-log secara lengkap di sisi server via slog.Error (termasuk detail MTA hostname, queue ID, banner SMTP) untuk keperluan forensik operator. Namun caller hanya menerima respons generik {"error":"send_failed","message":"failed to send email","message_id":"<uuid>"} — detail infrastruktur SMTP tidak pernah diteruskan ke response HTTP, mencegah information disclosure terhadap pihak yang memanggil service.
Error Lookup DB Tidak Bocor ke Caller
Handler handleMessageGet menerapkan pola yang sama: error database di-log via slog.ErrorContext dengan full detail (message ID + error string) untuk keperluan debugging, sedangkan caller hanya mendapat {"error":"lookup_failed","message":"internal error"}. Nama tabel, kolom, dan constraint PostgreSQL tidak pernah bocor ke respons HTTP.
Keamanan Environment & Konfigurasi
Systemd Environment Tidak Dapat Ditimpa File .env
email_service menggunakan godotenv.Load (bukan godotenv.Overload) untuk memuat file .env. Dengan semantik Load, variabel yang sudah di-set oleh systemd Environment= directive tidak akan pernah ditimpa oleh file .env yang berpotensi dikompromikan. Ini memastikan konfigurasi produksi yang di-inject oleh systemd selalu menjadi sumber kebenaran, bahkan jika file .env di disk berhasil dimodifikasi penyerang.
Keandalan Koneksi & Resource Management
Proteksi Goroutine SMTP dari Orphan
deliverViaSMTP() menggantikan smtp.SendMail standar dengan implementasi low-level yang context-aware:
- Dial TCP menggunakan
net.Dialer.DialContext(ctx, "tcp", addr)— dial menghormati parent context dan tidak akan menggantung selamanya. conn.SetDeadline(deadline)diterapkan per-langkah untuk setiap operasi I/O.- Watchdog goroutine memanggil
conn.Close()saatctx.Done()— ini memaksa semua blocking I/O dismtp.Client.{Mail,Rcpt,Data,Write,Quit}untuk unblock dengan error, sehingga goroutine pemanggil dipastikan exit. sendInstrumentedmembungkus seluruh proses dengancontext.WithTimeout(ctx, smtpSendTimeout)— lifetime goroutine terikat maksimal 30 detik.
Dengan implementasi ini, tidak akan terjadi akumulasi goroutine SMTP yang ter-orphan saat handler mengalami timeout, yang sebelumnya berpotensi menguras resource goroutine pool di bawah beban tinggi.
Body Size Limit pada Endpoint Internal
Middleware withBodyLimit (256 KiB) diterapkan untuk membungkus ketiga endpoint internal (/send, /log, /messages/) — mencegah memory exhaustion dari request oversized. Batas 256 KiB memberikan headroom yang luas dibandingkan payload paling besar yang legitimate (template staff_invitation penuh <4 KiB), sekaligus menjaga floor konsumsi memori tetap terkontrol.
Auth & Koneksi Aman
Constant-Time API Key Comparison
Middleware requireInternalKey menggunakan subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 untuk memvalidasi X-Internal-API-Key — mencegah timing side-channel attack yang memungkinkan penyerang merekonstruksi secret melalui perbedaan waktu respons. Meskipun deployment di loopback (127.0.0.1:8094) sudah mengurangi attack surface, pola constant-time adalah standar wajib untuk semua perbandingan shared-secret.
HTTP Server Timeout Guards
http.Server dikonfigurasi dengan timeout eksplisit: ReadHeaderTimeout: 5s (mitigasi Slowloris), ReadTimeout: 30s, WriteTimeout: 30s, IdleTimeout: 120s. Konfigurasi ini mencegah koneksi idle menggantung dan menguras resource di bawah serangan slow-client.
Penguatan Tambahan (2026-06-08)
- Idempotency mutual exclusion sebelum eksekusi:
Store.TryLock()menggunakan RedisSetNX(TTL 30s) untuk acquire distributed lock sebelum mengirim email — bukan setelah. Request kedua dengan idempotency key yang sama saat lock aktif mendapat 409 Conflict. Pola ini mencegah duplicate send bahkan dalam skenario concurrent retry yang bersamaan membaca cache miss.
Referensi
- Dokumen status lengkap:
services/email/email-service-status.md§9 dan §13 - Audit master:
merchant_docs/docs/plans/audit/KESLES_AUDIT_CLOSE.md - Arsitektur service:
services/email/architecture.md