Semua yang tiba di server lewat form — termasuk atribut HTML yang tampak permanen — pada akhirnya hanyalah teks dalam sebuah request HTTP yang bisa disusun dan dimodifikasi siapa pun. Developer tools browser mengedit hidden field dalam sekali klik, curl mengirim request tanpa form sama sekali, dan bot otomatis mengisi jutaan form per hari. Chapter penutup section ini merangkum praktik mengamankan form dari sudut pandang batas kepercayaan (trust boundary), lalu melatih beberapa teknik pertahanan yang bisa diuji langsung. Pembahasan keamanan yang mendalam per topik tinggal menunggu di Section Web Security .

Batas Kepercayaan Form #

Bayangkan garis imajiner antara browser dan server: di luar garis itu, semua data adalah masukan dari orang asing. Atribut maxlength, required, disabled, hidden field, dan validasi JavaScript hidup di luar garis tersebut — fungsinya membantu pengguna yang baik, bukan menghentikan penyerang. Di dalam garis, server wajib memvalidasi ulang semuanya seolah form yang kita kirim sendiri tidak pernah ada. Prinsip ini menjelaskan mengapa chapter-chapter sebelumnya menempatkan validasi di server: Validasi Server-Side adalah fondasi, dan teknik-teknik berikut adalah lapisan tambahannya.

maxlength di Klien vs di Server #

Atribut maxlength mencegah pengguna baik memaksa keyboard mereka mengetik terlalu jauh. Ia tidak mencegah apa pun terhadap request rakitan:

php
<?php
function validasiCatatan(string $catatan): ?string
{
    if (trim($catatan) === '') {
        return 'Catatan wajib diisi.';
    }
    if (mb_strlen($catatan) > 200) {
        return 'Catatan maksimal 200 karakter.';
    }
    return null;
}

// <input maxlength="200"> hanya saran bagi browser yang kooperatif;
// curl dan skrip dapat mengirim teks sepanjang apa pun
$catatanDariCurl = str_repeat('promo ', 100);

echo 'Panjang teks terkirim: ' . mb_strlen($catatanDariCurl) . "\n";
echo validasiCatatan($catatanDariCurl) . "\n";

Output yang Dihasilkan:

text
Panjang teks terkirim: 600
Catatan maksimal 200 karakter.

Teks rakitan sepanjang 600 karakter melewati atribut maxlength="200" tanpa hambatan karena atribut itu hanya hidup di dalam browser — curl menyusun body request secara manual dan sama sekali tidak membaca HTML kita. Pemeriksaan mb_strlen() di server menolaknya tanpa kompromi. Pola yang sama berlaku untuk semua atribut pembatas: min, max, pattern, type="number" — semuanya saran, bukan tembok.

Hidden Field Bisa Dimodifikasi #

Hidden field sering dipakai untuk “membawa” data antar halaman: id produk, harga, bahkan peran pengguna. Semuanya bisa diedit bebas sebelum terkirim. Perbandingan berikut menunjukkan akibat memercayai harga dari form:

php
<?php
// Sumber kebenaran harga ada di server, bukan di form
$hargaProduk = [
    'p-001' => ['nama' => 'Buku Belajar PHP', 'harga' => 150000],
    'p-002' => ['nama' => 'Kamus Istilah IT', 'harga' => 250000],
];

function hitungTotalAman(string $idProduk, int $jumlah, array $hargaProduk): int
{
    if (!array_key_exists($idProduk, $hargaProduk)) {
        throw new InvalidArgumentException("Produk tidak dikenal: {$idProduk}");
    }
    return $hargaProduk[$idProduk]['harga'] * $jumlah;
}

// Penyerang memodifikasi hidden field "harga" menjadi 100
$postDimodifikasi = ['id_produk' => 'p-001', 'harga' => 100, 'jumlah' => 2];

// Versi ceroboh: memercayai harga yang ikut dikirim form
$totalCeroboh = (int) $postDimodifikasi['harga'] * (int) $postDimodifikasi['jumlah'];

// Versi aman: harga dibaca dari data di server
$totalAman = hitungTotalAman(
    $postDimodifikasi['id_produk'],
    (int) $postDimodifikasi['jumlah'],
    $hargaProduk,
);

echo "Total versi ceroboh: Rp{$totalCeroboh}\n";
echo "Total versi aman: Rp{$totalAman}\n";

Output yang Dihasilkan:

text
Total versi ceroboh: Rp200
Total versi aman: Rp300000

Versi ceroboh menghasilkan Rp200 karena menghitung dengan harga palsu yang dikirim bersama POST; versi aman menghasilkan Rp300000 karena form hanya dipercaya membawa id_produk dan jumlah, sedangkan harga dibaca dari tabel di server. Aturan praktisnya: hidden field boleh membawa identifier (id, token, nonce), tidak boleh membawa otoritas (harga, diskon, peran, saldo). Id yang tidak dikenal pun ditolak eksplisit lewat pengecekan array_key_exists() — sama polanya dengan membaca data eksternal apa pun.

Umpan Spam Berupa Honeypot #

Form publik menerima kunjungan bot yang mengisi semua field yang ditemukannya. Honeypot (“periuk madu”) memanfaatkan kebiasaan itu dengan menyediakan field yang kelihatan wajar bagi bot tetapi tidak terlihat oleh manusia:

html
<!-- CSS menyembunyikan kotak ini dari manusia -->
<div class="kolom-samping" aria-hidden="true">
    <label for="website_url">Website</label>
    <input type="text" id="website_url" name="website_url"
           tabindex="-1" autocomplete="off">
</div>
css
.kolom-samping {
    position: absolute;
    left: -9999px;
}

Atribut aria-hidden="true" memastikan pembaca layar melewati field ini, dan tabindex="-1" menjauhkannya dari navigasi keyboard — manusia nyaris mustahil sampai ke field tersebut. Pemeriksaannya sederhana:

php
<?php
function terdeteksiSpamHoneypot(array $post): bool
{
    // Field umpan: disembunyikan dengan CSS, manusia normal tidak mengisinya
    $umpan = trim((string) ($post['website_url'] ?? ''));
    return $umpan !== '';
}

$submitManusia = ['nama' => 'Siti', 'pesan' => 'Halo!', 'website_url' => ''];
$submitBot     = ['nama' => 'Siti', 'pesan' => 'Beli obat ajaib', 'website_url' => 'http://spam.example'];

var_dump(terdeteksiSpamHoneypot($submitManusia));
var_dump(terdeteksiSpamHoneypot($submitBot));

Output yang Dihasilkan:

text
bool(false)
bool(true)

Submit pertama lolos karena field umpan kosong; submit kedua ditandai sebagai spam karena field tersembunyi itu terisi — tingkah laku yang hanya masuk akal untuk bot. Honeypot bukan pertahanan sempurna: bot modern bisa membaca CSS dan melewati field tersembunyi, jadi perlakukan hasilnya sebagai sinyal kuat untuk menolak diam-diam, bukan bukti hukum. Kombinasikan dengan CAPTCHA atau verifikasi lain untuk form bernilai tinggi.

Rate Limiting Sederhana #

Validasi menyaring isi submit; rate limiting menyaring laju submit. Bentuk paling dasarnya: catat timestamp tiap submit di sisi server, lalu tolak bila jumlahnya melebihi batas dalam jendela waktu tertentu. Logikanya sebagai fungsi murni:

php
<?php
function bolehKirim(array $riwayatTimestamp, int $waktuSekarang, int $batas = 3, int $jendelaDetik = 60): bool
{
    $submitTerkini = array_filter(
        $riwayatTimestamp,
        fn (int $timestamp) => $timestamp > $waktuSekarang - $jendelaDetik,
    );
    return count($submitTerkini) < $batas;
}

// Riwayat submit yang disimpan di sisi server (mis. per akun di session)
$riwayatSubmit = [1000, 1010, 1020];

var_dump(bolehKirim($riwayatSubmit, 1030));            // 3 submit dalam 60 detik terakhir
var_dump(bolehKirim($riwayatSubmit, 1030, batas: 5));  // batas lebih longgar
var_dump(bolehKirim($riwayatSubmit, 2000));            // jendela waktu sudah lewat

Output yang Dihasilkan:

text
bool(false)
bool(true)
bool(true)

Pada detik ke-1030, ketiga submit lama (detik 1000, 1010, 1020) masih berada dalam jendela 60 detik sehingga submit keempat ditolak; batas yang dilonggarkan mengizinkannya; dan pada detik ke-2000 seluruh jendela telah bergeser sehingga riwayat lama tidak dihitung lagi. Ini pola sliding window — jendela waktu bergerak mengikuti waktu sekarang. Implementasi nyata menyimpan riwayat per identitas (akun, session, atau alamat IP) di session atau penyimpanan seperti Redis, dan penanganan konkurensi benerannya perlu atomisitas yang tidak bisa diberikan array biasa — konsep penyimpanannya menyusul di Section Caching dan Performance .

Sekilas CSRF #

Satu ancaman khusus menyerang form lewat jalur tak terduga: penyerang membuat halaman jahat yang membuat browser pengguna yang sudah login mengirim POST ke aplikasi kita tanpa sepengetahuan pengguna. Pertahanannya adalah CSRF token: nilai rahasia per session yang ikut disisipkan di setiap form dan diverifikasi ulang di server:

php
<?php
// Sketsa pemeriksaan token CSRF - bahasan lengkap ada di chapter CSRF Tokens
function tokenCsrfValid(?string $tokenForm, ?string $tokenSession): bool
{
    if ($tokenForm === null || $tokenSession === null) {
        return false;
    }
    return hash_equals($tokenSession, $tokenForm);
}

$tokenSession = bin2hex(random_bytes(32)); // nilai contoh, disimpan per session

var_dump(tokenCsrfValid('tebakan-penyerang', $tokenSession));
var_dump(tokenCsrfValid($tokenSession, $tokenSession));

Output yang Dihasilkan:

text
bool(false)
bool(true)

Dua detail kecil yang besar artinya: token dihitung dengan random_bytes() sehingga tidak bisa ditebak situs lain, dan perbandingannya memakai hash_equals() yang lambat secara sengaja agar waktu pemeriksaan tidak membocorkan isi token. Sketsa ini sengaja dipadatkan — mekanisme lengkapnya, tempat token disimpan, dan cara form memakainya dibedah tuntas di chapter CSRF Tokens .

Rekap Pertahanan Berlapis #

Tidak ada satu tombol ajaib untuk mengamankan form; yang ada adalah lapisan-lapisan yang saling menutupi celah satu sama lain:

AncamanPertahananPendalaman
Input melebihi batasValidasi panjang di serverValidasi Server-Side
Hidden field dimodifikasiData penting hanya dihitung serverChapter ini
Spam bot massalHoneypot dan rate limitingChapter ini
XSS lewat output formhtmlspecialchars() di semua outputOutput Escaping
Upload berkas berbahayaValidasi error, size, ekstensi, finfoMenangani File Upload
Submit gandaPost/Redirect/GetPost/Redirect/Get
Request palsu antar situsCSRF token per sessionCSRF Tokens
Serangan lewat query databasePrepared statementsSQL Injection

Dengan rekap ini, perjalanan kita memproses form tuntas: dari membaca input, memvalidasi, menampilkan error, mengatur alur submit, sampai mengamankan akarnya. Aplikasi yang mampu mengingat siapa penggunanya adalah langkah berikutnya — dan untuk itu kita perlu memahami sifat dasar HTTP yang stateless di Section HTTP State: Sessions dan Cookies .

Referensi #