Ilustrasi flat vector perisai dengan tanda centang menjaga kartu formulir sederhana

Episode #59 kemarin kamu sudah bisa menerima data dari user lewat form. Sekarang pertanyaan pentingnya: apakah data itu bisa dipercaya? Jawabannya: tidak. User bisa mengisi nama dengan 500 karakter, umur dengan tulisan "tua banget", atau email tanpa tanda @. Lebih parah lagi, user jahat bisa menyisipkan kode berbahaya lewat form. Episode #60 ini tentang validasi form: cara memastikan data yang masuk akal sehat sebelum kamu pakai.

Posisinya di perjalanan belajar: ini episode keamanan pertamamu di series PHP Native. Pola yang kamu pelajari di sini, validasi di input dan escaping di output, akan dipakai di semua episode berikutnya sampai Laravel.

Persepsi yang Diluruskan: Validasi HTML Saja Tidak Cukup

Mitos paling berbahaya: "form saya sudah pakai required dan type=email di HTML, jadi aman". Salah besar. Validasi di HTML (client-side) itu cuma saran baik untuk user jujur. Siapa pun bisa membuka developer tools, menghapus atribut required, dan mengirim data apa pun. Atau lebih gampang: kirim request POST langsung pakai curl tanpa buka browser sama sekali.

Analoginya seperti satpam mal. Validasi HTML itu seperti tulisan "dilarang membawa makanan" di pintu: dipatuhi orang baik-baik, diabaikan orang yang niat jahat. Validasi server-side (di PHP) itu pemeriksaan tas beneran: siapa pun yang masuk, diperiksa. Aturan emas keamanan web: jangan pernah percaya input dari client. Semua yang datang dari browser harus dianggap kotor sampai terbukti bersih di server.

Jadi validasi HTML tetap dipakai (bagus untuk UX, memberi feedback instan), tapi validasi PHP tidak boleh absen. Keduanya punya peran beda: HTML untuk kenyamanan, PHP untuk keamanan.

Tiga Pekerjaan Berbeda: Validasi, Sanitasi, Escaping

Pemula sering mencampur tiga istilah ini jadi satu. Padahal beda pekerjaan, beda waktu pengerjaannya.

Validasi = memeriksa apakah input memenuhi aturan, dan menolak kalau tidak. "Email ini formatnya benar tidak? Umur di rentang wajar tidak?" Kalau tidak lolos, kasih tahu user dan suruh perbaiki. Validasi tidak mengubah data, ia memberi vonis: terima atau tolak.

Sanitasi = membersihkan input ke bentuk yang wajar, secukupnya saja. Contoh: trim() membuang spasi di awal dan akhir (" Adit " jadi "Adit"). Jangan berlebihan: diam-diam mengubah isi ketikan user sering menyembunyikan bug.

Escaping = mengamankan data saat DITAMPILKAN, sesuai tempat tujuannya. Mau tampil di HTML? Bungkus dengan htmlspecialchars(). Mau masuk ke query SQL? Pakai prepared statement (episode #62). Escaping terjadi di output, bukan di input.

Kalimat kuncinya, hafalkan: validasi saat input, escaping saat output. Kebanyakan tutorial jadul mengajarkan "bersihkan semua input dengan satu fungsi ajaib", itu pola yang salah dan sudah ditinggalkan.

Kenapa htmlspecialchars Itu Penting: Serangan XSS

Bayangkan form komentar tanpa pengamanan. User jahat mengisi komentar dengan <script>alert('kena!')</script>. Kalau komentar itu ditampilkan apa adanya, browser menganggapnya kode JavaScript beneran dan menjalankannya. Itu XSS (Cross-Site Scripting): menyuntikkan script lewat input yang ditampilkan kembali.

Dalam contoh ini cuma alert iseng, tapi script suntikan bisa mencuri cookie sesi, mengalihkan ke situs phishing, atau mengubah isi halaman. Solusinya satu baris:

<?php
$komentar = $_POST["komentar"] ?? "";
echo htmlspecialchars($komentar, ENT_QUOTES, 'UTF-8');
?>

htmlspecialchars mengubah karakter spesial HTML jadi entitas yang tidak berbahaya: < jadi &lt;, > jadi &gt;, tanda kutip juga diamankan. Browser menampilkan teksnya apa adanya sebagai tulisan, bukan mengeksekusinya sebagai kode. Script suntikan lumpuh total, tinggal jadi teks pajangan.

Perhatikan: kita tidak mengubah data aslinya di variabel, kita mengamankannya saat ditampilkan. Kalau suatu saat komentar itu mau ditampilkan di tempat lain (misal API JSON), perlakuannya beda lagi. Itulah kenapa escaping di output, bukan di input.

Berhenti dan tebak dulu! User mengisi nama dengan <b>Adit</b>. Apa yang tampil di browser untuk masing-masing baris?

<?php
$nama = $_POST["nama"] ?? ""; // isi: <b>Adit</b>
echo $nama;
echo htmlspecialchars($nama);
?>
Jawaban: baris pertama menampilkan "Adit" dengan huruf tebal (bold), baris kedua menampilkan teks mentah "<b>Adit</b>". Baris pertama berbahaya: tag HTML dari user dirender browser apa adanya. Kalau itu tag script, kode jahat jalan. Baris kedua aman: tag diubah jadi teks biasa. Mulai sekarang, setiap echo data dari user WAJIB lewat htmlspecialchars. Tanpa kecuali.

Pola Validasi Lengkap: Kumpulkan Error Dulu

Sekarang rakit semuanya jadi pola validasi form yang rapi. Strateginya: cek satu per satu, kumpulkan semua error ke array, kalau array kosong berarti data bersih dan boleh dipakai.

<?php
$errors = [];
$nama = trim($_POST["nama"] ?? "");
$email = trim($_POST["email"] ?? "");
$umur = $_POST["umur"] ?? "";

// Aturan 1: nama wajib diisi, minimal 3 karakter
if ($nama === "") {
  $errors[] = "Nama wajib diisi.";
} elseif (mb_strlen($nama) < 3) {
  $errors[] = "Nama minimal 3 karakter.";
}

// Aturan 2: email harus format email beneran
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
  $errors[] = "Format email tidak valid.";
}

// Aturan 3: umur harus angka 1-120
if (!filter_var($umur, FILTER_VALIDATE_INT, ["options" => ["min_range" => 1, "max_range" => 120]])) {
  $errors[] = "Umur harus angka antara 1 sampai 120.";
}

if (empty($errors)) {
  echo "Pendaftaran berhasil. Halo, " . htmlspecialchars($nama) . "!";
} else {
  foreach ($errors as $e) {
    echo "<p>" . htmlspecialchars($e) . "</p>";
  }
}
?>

Bedah alur berpikirnya. Kenapa error dikumpulkan dulu, bukan langsung die saat ketemu error pertama? Karena UX: user lebih senang melihat semua kesalahannya sekaligus ("nama kurang panjang, email salah format") daripada diperintah perbaiki satu per satu bolak-balik submit. Pola kumpulkan-dulu-tampilkan-sekaligus adalah standar industri.

Kenapa pakai filter_var dengan FILTER_VALIDATE_EMAIL? Karena regex email yang benar itu rumit dan gampang salah tulis. PHP sudah menyediakan validator bawaan yang teruji, pakai itu. Jangan bikin regex email sendiri kecuali kamu paham betul RFC-nya.

Kenapa umur divalidasi sebagai integer dengan rentang, bukan cuma "tidak kosong"? Karena input form selalu datang sebagai string, dan user bisa mengisi "dua puluh" atau "9999". Validasi yang baik memeriksa tipe DAN rentang yang masuk akal. Data yang lolos validasi ketat jarang menimbulkan bug di downstream.

Berhenti dan tebak dulu! User mengisi umur dengan " 25 " (ada spasi di awal dan akhir). Lolos validasi atau tidak?

<?php
$umur = $_POST["umur"] ?? ""; // isi: " 25 "
if (!filter_var($umur, FILTER_VALIDATE_INT)) {
  echo "tidak valid";
} else {
  echo "valid";
}
?>
Jawaban: tidak valid. FILTER_VALIDATE_INT menolak string berisi spasi. Makanya di pola lengkap di atas kita trim() dulu sebelum validasi: " 25 " dibersihkan jadi "25" baru divalidasi. Urutan yang benar: trim dulu (sanitasi ringan), lalu validasi. Spasi nyasar adalah input ngawur paling umum di dunia nyata, dan trim adalah obat murahnya.

Kuis cepat: Mana pernyataan yang benar soal validasi?

Latihan dan Langkah Berikutnya

Latihan 1. Ambil file biodata.php dari latihan episode #59, lalu TAMBAHKAN validasi lengkap: nama minimal 3 karakter, umur integer 1-120, kota tidak boleh kosong. Kumpulkan error ke array dan tampilkan semuanya sekaligus kalau ada. Jangan tampilkan biodata kalau masih ada error.

Latihan 2. Bikin file komentar.php: form textarea untuk komentar. Setelah disubmit, tampilkan komentarnya. Coba isi dengan <script>alert('xss')</script> dan buktikan alert TIDAK muncul karena kamu pakai htmlspecialchars. Lalu coba hapus htmlspecialchars-nya dan lihat alert muncul. Rasakan bedanya, lalu kembalikan pengamannya.

Latihan 3. TAMBAHKAN satu aturan validasi baru ke pola di artikel ini: field password minimal 8 karakter dan harus mengandung minimal satu angka (petunjuk: pakai preg_match('/[0-9]/', $password)). Pikirkan: kenapa aturan password dicek di server, bukan cuma di HTML?

Di episode #61 kita belajar cara PHP "mengingat" user: session dan cookie, plus cara menerima upload file. Di sanalah kita bikin login sederhana pertama. Tapi ingat, login tanpa validasi yang kamu pelajari hari ini ibarat gembok tanpa pintu. Sampai jumpa.