Email adalah vektor penipuan tertua di internet, dan pengguna telah belajar curiga terhadap hampir setiap pesan yang masuk. Jika email aplikasi Anda mudah dipalsukan, seluruh fitur yang bergantung padanya — verifikasi akun, reset password, notifikasi transaksi — kehilangan nilainya dalam sekejap. Chapter ini membahas keamanan email dari tiga arah: autentikasi domain lewat SPF, DKIM, dan DMARC agar email Anda tidak mudah dipalsukan; pengamanan header dari input pengguna; serta tautan bertanda tangan yang tidak bisa dimanipulasi penyerang. Bagian yang dapat diuji secara lokal — sanitasi input, HMAC, dan verifikasi token — kita praktikkan langsung dengan data tetap.
Fondasi Autentikasi: SPF, DKIM, dan DMARC #
Tiga mekanisme DNS inilah yang membuat penerima yakin email Anda benar-benar berasal dari domain Anda. SPF (Sender Policy Framework) mendaftarkan IP address yang diizinkan mengirim untuk domain Anda; DKIM (DomainKeys Identified Mail) menandatangani isi email dengan kunci privat yang kunci publiknya dipublikasikan di DNS; DMARC (Domain-based Message Authentication, Reporting and Conformance) mengikat keduanya dengan kebijakan tentang domain From serta menyediakan kanal laporan. Tabel berikut merangkum pembagian perannya:
| Mekanisme | Record DNS | Yang diverifikasi | Jika gagal |
|---|---|---|---|
| SPF | TXT pada root domain | IP pengirim diizinkan oleh domain amplop | Kiriman ditolak atau masuk spam |
| DKIM | TXT pada selector kunci | Isi email cocok dengan signature kunci privat | Signature dianggap tidak valid |
| DMARC | TXT pada _dmarc | Alignment SPF/DKIM dengan domain From + kebijakan | Ditolak / dilaporkan sesuai kebijakan |
Ketiganya berupa record TXT yang dapat diperiksa dari PHP lewat dns_get_record(). Contoh berikut tidak dieksekusi di lingkungan belajar ini karena hasil query DNS berubah antar eksekusi dan bergantung pada koneksi internet:
<?php
// Membutuhkan koneksi DNS internet - output berubah antar eksekusi,
// jadi tidak dijalankan di lingkungan belajar ini.
$records = dns_get_record("php.net", DNS_TXT);
foreach ($records as $record) {
echo $record["txt"], "\n";
}Konfigurasi ketiga record tersebut dilakukan di panel DNS domain Anda, bukan di kode PHP — nilai SPF berbentuk v=spf1 include:spf.provider-anda.com -all, DKIM menghasilkan pasangan kunci dari penyedia email, dan DMARC dimulai dari kebijakan monitoring p=none sebelum diperketat bertahap ke quarantine lalu reject. Detail spesifikasi ketiganya dapat dibaca pada RFC 7208 (SPF), RFC 6376 (DKIM), dan RFC 7489 (DMARC) yang tercantum di bagian Referensi.
Mencegah Header Injection dari Input Pengguna #
Serangan paling langsung terhadap aplikasi pengirim email adalah header injection: penyerang menyisipkan karakter carriage return dan line feed (\r\n) ke dalam input yang nantinya menjadi bagian header, lalu menambahkan baris header palsu seperti Bcc: untuk mengirim email ke ribuan alamat korban. Pertahanan pertamanya sederhana — buang kedua karakter tersebut dari setiap nilai yang masuk ke header — dan dilengkapi validasi alamat email dengan filter_var():
<?php
function sanitizeHeaderField(string $value): string
{
return trim(str_replace(["\r", "\n"], "", $value));
}
$namaPengguna = "Budi\r\nBcc: [email protected]";
echo "Nama asli : " . json_encode($namaPengguna) . "\n";
echo "Nama bersih : " . json_encode(sanitizeHeaderField($namaPengguna)) . "\n";
$emailBerbahaya = "[email protected]\r\nBcc: [email protected]";
$valid = filter_var($emailBerbahaya, FILTER_VALIDATE_EMAIL);
echo "Email berbahaya lolos validasi: " . var_export($valid, true) . "\n";Output yang Dihasilkan:
Nama asli : "Budi\r\nBcc: [email protected]"
Nama bersih : "BudiBcc: [email protected]"
Email berbahaya lolos validasi: falseBaris json_encode() sengaja dipakai untuk mencetak kedua versi nama karena json_encode() menampilkan karakter kontrol \r\n secara eksplisit, sehingga Anda bisa melihat apa yang sebenarnya tersimpan di dalam string. Setelah sanitasi, teks Bcc: masih tersisa sebagai isi nama tetapi tidak lagi berada di baris baru — tanpa line break, teks tersebut tidak pernah ditafsirkan sebagai header oleh mail server. Baris terakhir membuktikan lapisan kedua: filter_var() dengan FILTER_VALIDATE_EMAIL menolak seluruh alamat yang mengandung karakter kontrol, mengembalikan false.
Menandatangani Isi Email dengan HMAC #
DKIM bekerja dengan prinsip tanda tangan: pengirim menghitung hash terenkripsi atas bagian-bagian email, penerima memverifikasi ulang hash tersebut dengan kunci publik di DNS, dan isi yang berubah sepeser pun membuat verifikasi gagal. Anda dapat memahami inti mekanisme tersebut dengan hash_hmac() pada data tetap — fungsi menghitung signature berbasis kunci rahasia, dan perubahan satu karakter pada input menghasilkan signature yang sama sekali berbeda:
<?php
function signBody(string $body, string $secret): string
{
return hash_hmac("sha256", $body, $secret);
}
$bodyAsli = "Halo Budi,\n\nTerima kasih sudah berbelanja di Toko Budi.\nTotal pesanan Anda: Rp250.000\n";
$secret = "YOUR_DKIM_SECRET";
$signatureAsli = signBody($bodyAsli, $secret);
$bodyDimodifikasi = str_replace("Rp250.000", "Rp2.500.000", $bodyAsli);
$signatureDimodifikasi = signBody($bodyDimodifikasi, $secret);
echo "Signature body asli : {$signatureAsli}\n";
echo "Signature body diubah : {$signatureDimodifikasi}\n";
echo "Kedua signature cocok : " . (hash_equals($signatureAsli, $signatureDimodifikasi) ? "ya" : "tidak") . "\n";Output yang Dihasilkan:
Signature body asli : 8a63f7c15b98cfa837bb735112ad8206741ddcd44ed10d9f6101c74e84375ee3
Signature body diubah : 606b9ae2a04fc9038a43557aae473486bcd1049c8d837f5319c07fd566175ec0
Kedua signature cocok : tidakProgram menandatangani body asli, lalu menandatangani versi yang totalnya diubah dari Rp250.000 menjadi Rp2.500.000 — skenario manipulasi di tengah jalan yang harus dideteksi sistem pembayaran. Meski perbedaan kedua body hanya lima karakter, kedua signature tidak memiliki kesamaan sama sekali, dan perbandingan dengan hash_equals() mengembalikan tidak. Perlu dicatat perbedaannya dengan DKIM sesungguhnya: DKIM memakai asymmetric cryptography (kunci privat menandatangani, kunci publik di DNS memverifikasi) dan menandatangani header terpilih plus body, sedangkan demo HMAC ini memakai shared secret. Prinsip inti identik: penerima dapat membuktikan isi tidak berubah sejak ditandatangani.
Tautan Bertanda Tangan dengan Masa Berlaku #
Tautan dalam email — reset password, verifikasi akun, unduhan — adalah target favorit manipulasi karena mudah dipalsukan dengan mengubah satu parameter. Solusinya adalah menandatangani seluruh query string dengan HMAC dan menyertakan masa berlaku di dalam tanda tangan itu sendiri, sehingga parameter yang diubah atau tautan yang kedaluwarsa sama-sama gagal verifikasi:
<?php
function buildSignedLink(string $baseUrl, array $params, string $secret, DateTimeImmutable $expiresAt): string
{
$params["expires"] = $expiresAt->getTimestamp();
ksort($params);
$query = http_build_query($params);
$signature = hash_hmac("sha256", $query, $secret);
return "{$baseUrl}?{$query}&signature={$signature}";
}
function verifySignedLink(array $queryParams, string $secret, DateTimeImmutable $now): bool
{
$signature = $queryParams["signature"] ?? "";
unset($queryParams["signature"]);
ksort($queryParams);
$query = http_build_query($queryParams);
$expected = hash_hmac("sha256", $query, $secret);
if (!hash_equals($expected, $signature)) {
return false;
}
$expires = (int) ($queryParams["expires"] ?? 0);
return $now->getTimestamp() <= $expires;
}
$secret = "YOUR_LINK_SECRET";
$now = new DateTimeImmutable("2026-01-15 08:00:00", new DateTimeZone("Asia/Jakarta"));
$link = buildSignedLink(
"https://tokobudi.example.com/reset-password",
["user_id" => "1042"],
$secret,
$now->modify("+2 hours")
);
echo $link . "\n";
parse_str((string) parse_url($link, PHP_URL_QUERY), $queryParams);
echo "Token asli, belum kadaluarsa : ";
echo verifySignedLink($queryParams, $secret, $now->modify("+1 hour")) ? "valid" : "tidak valid";
echo "\n";
echo "Token asli, sudah kadaluarsa : ";
echo verifySignedLink($queryParams, $secret, $now->modify("+3 hours")) ? "valid" : "tidak valid";
echo "\n";
$queryParams["user_id"] = "1";
echo "Token dipalsukan : ";
echo verifySignedLink($queryParams, $secret, $now->modify("+1 hour")) ? "valid" : "tidak valid";
echo "\n";Output yang Dihasilkan:
https://tokobudi.example.com/reset-password?expires=1768446000&user_id=1042&signature=267c9fe5dd5bab1cd9a40920ffc56db7805a21ead3104ee0757988faf5f0bd2f
Token asli, belum kadaluarsa : valid
Token asli, sudah kadaluarsa : tidak valid
Token dipalsukan : tidak validMari kita bedah mekanismenya langkah demi langkah:
buildSignedLink()menambahkan stempel waktuexpireske parameter, mengurutkan kunci denganksort()agar urutan pembentukan query selalu konsisten, lalu menandatangani hasilhttp_build_query()dengan HMAC. Signature menempel di akhir URL sebagai parametersignature.verifySignedLink()memisahkan signature, membangun ulang query dari parameter tersisa dengan urutan yang sama, lalu menghitung HMAC yang diharapkan. Perbandingan memakaihash_equals()untuk kebal serangan timing.- Verifikasi kedua arah: signature yang cocok belum cukup — stempel
expiresdiperiksa terhadap waktu sekarang, sehingga tautan yang bocor pun kehilangan dayanya setelah masa berlaku habis. - Baris percobaan ketiga mengubah
user_iddari1042menjadi1(mencoba masuk sebagai pengguna lain) tanpa menghitung ulang signature. Query yang dibangun ulang tidak lagi sama dengan query yang ditandatangani, sehingga verifikasi gagal.
Waktu pada contoh sengaja dibekukan pada 2026-01-15 08:00:00 agar hasil eksekusi selalu identik; di production, ganti nilai tersebut dengan new DateTimeImmutable("now"). Prinsip tambahan yang tidak kalah penting: token seperti ini harus sekali pakai — setelah verifikasi berhasil, simpan status “sudah dipakai” di database agar tautan valid tidak bisa digunakan berulang.
Header List-Unsubscribe untuk One-Click #
Pada chapter Notification Preferences kita membahas endpoint unsubscribe satu klik dari sisi keputusan; di sini kita lengkapi sisi penyisipannya ke dalam email. Dua header berikut membuat aplikasi email menampilkan tombol bawaan “Unsubscribe” yang bekerja dengan satu klik:
<?php
function buildUnsubscribeHeaders(string $baseUrl, string $payload, string $secret): array
{
$token = hash_hmac("sha256", $payload, $secret);
return [
"List-Unsubscribe" => "<{$baseUrl}/unsubscribe?payload=" . urlencode($payload) . ">, <mailto:[email protected]>",
"List-Unsubscribe-Post" => "List-Unsubscribe=One-Click",
];
}
$headers = buildUnsubscribeHeaders(
"https://tokobudi.example.com",
base64_encode('{"user_id":"budi","type":"marketing"}'),
"YOUR_UNSUBSCRIBE_SECRET"
);
foreach ($headers as $name => $value) {
echo "{$name}: {$value}\n";
}Output yang Dihasilkan:
List-Unsubscribe: <https://tokobudi.example.com/unsubscribe?payload=eyJ1c2VyX2lkIjoiYnVkaSIsInR5cGUiOiJtYXJrZXRpbmcifQ%3D%3D>, <mailto:[email protected]>
List-Unsubscribe-Post: List-Unsubscribe=One-ClickHeader List-Unsubscribe berisi dua URI: URL endpoint bertanda tangan (payload terenkode base64, token HMAC dihitung oleh endpoint saat verifikasi) dan alternatif mailto: bagi penyedia yang belum mendukung satu klik. Header List-Unsubscribe-Post memberi tahu penyedia email bahwa endpoint menerima POST dengan body List-Unsubscribe=One-Click sesuai RFC 8058. Menyediakan mekanisme ini bukan hanya kepatuhan hukum — penyedia email besar menjadikan keberadaannya sebagai salah satu sinyal pengirim yang bertanggung jawab.
Kebersihan Konten Email Keamanan #
Lapisan terakhir keamanan ada pada isi email itu sendiri. Email notifikasi keamanan seperti “password Anda berhasil diganti” sering dipalsukan penyerang untuk memancing korban mengeklik tautan reset palsu — justru karena korban sedang panik. Karena itu email keamanan Anda tidak boleh menyertakan tautan reset password; cukup minta pengguna membuka situs secara manual dengan mengetik alamatnya sendiri, dan sertakan petunjuk waktu serta perangkat peristiwa agar pengguna bisa menilai kejadian.
Bebiasa lain yang perlu dijaga: gunakan satu domain pengirim yang konsisten untuk semua email transaksional (jangan meniru domain pihak ketiga), tampilkan nama pengirim yang jujur tanpa menyerupai layanan populer, dan hindari gambar remote yang melacak pembukaan email. Konten HTML bersih dengan teks alternatif yang lengkap membuat email Anda tetap terbaca di semua aplikasi penerima sekaligus menurunkan skor phishing.
Ringkasan #
Keamanan email berdiri di atas fondasi autentikasi domain (SPF, DKIM, DMARC), header yang bersih dari injeksi, dan tautan yang ditandatangani beserta masa berlakunya. Prinsip HMAC yang menyatukan DKIM dan signed link terbukti sederhana diuji secara lokal: satu karakter yang berubah meruntuhkan seluruh verifikasi. Ditambah kebiasaan konten yang tidak membuka celah phishing, email aplikasi Anda layak dipercaya penerima. Pada titik ini kita juga telah berkali-kali menyinggung worker yang mengirim email di latar belakang — pola di balik worker tersebut adalah tema besar Section Asynchronous Processing , yang dimulai dari memahami background jobs.