Lewati ke konten utama

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() saat ctx.Done() — ini memaksa semua blocking I/O di smtp.Client.{Mail,Rcpt,Data,Write,Quit} untuk unblock dengan error, sehingga goroutine pemanggil dipastikan exit.
  • sendInstrumented membungkus seluruh proses dengan context.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 Redis SetNX (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