Setiap byte yang masuk ke aplikasi kita — form, parameter URL, isi file, bahkan balasan API pihak ketiga — adalah data yang tidak dikontrol. Prinsip never trust input dari Chapter Secure Coding Principles hanya berguna jika kita tahu cara menegakkannya dalam kode. Chapter ini membahas dua teknik dasarnya: validasi yang menolak data di luar spesifikasi, dan sanitasi yang membersihkan data sebelum dipakai.

Validasi vs Sanitasi #

Kedua istilah ini sering tertukar, padahal keduanya menjawab pertanyaan yang berbeda:

AspekValidasiSanitasi
Pertanyaan“Apakah data ini sah?”“Bisakah data ini dibersihkan?”
Tindakan saat bermasalahTolak seluruh inputUbah lalu terima
ContohField usia harus angka 13-120Karakter < diubah menjadi &lt;
Kapan dipakaiData punya format ketat (angka, tanggal, pilihan)Data bebas seperti komentar

Validasi lebih aman karena tidak pernah menebak maksud penyerang. Sanitasi dipakai ketika input memang harus bebas bentuknya — komentar pengguna, misalnya. Keduanya bukan pilihan salah satu: aplikasi nyata menggabungkan keduanya sebagai lapisan pertahanan.

Allowlist Mengalahkan Blocklist #

Cara paling intuitif memfilter input adalah blocklist: daftar hal buruk yang diblokir. Masalahnya, daftar hal buruk tidak pernah lengkap. Perhatikan komentar yang menyusup dengan tag <scr!pt> — sengaja salah eja agar lolos dari blok <script>:

python
import re

komentar = "<scr!pt>Produknya bagus sekali</scr!pt>"

# Blocklist: hanya memblokir tag yang sudah diketahui
blokir = ["<script>", "</script>"]
if any(tag in komentar.lower() for tag in blokir):
    print("Blocklist: komentar ditolak")
else:
    print("Blocklist: komentar diterima (bahaya!)")

# Allowlist: hanya karakter tertentu yang boleh ada
pola = re.compile(r"^[a-zA-Z0-9 .,!?-]+$")
if pola.fullmatch(komentar):
    print("Allowlist: komentar diterima")
else:
    print("Allowlist: komentar ditolak")

Output yang Dihasilkan:

text
Blocklist: komentar diterima (bahaya!)
Allowlist: komentar ditolak

Mari kita bedah kode di atas:

  1. Baris 5-6 (blokir = ...): blocklist hanya mengenal dua bentuk tag. Karakter ! di dalam <scr!pt> membuat pencocokan string gagal, sehingga komentar lolos.
  2. Baris 13 (pola = re.compile(...)): allowlist bekerja sebaliknya — regex mendefinisikan karakter sah (huruf, angka, spasi, sejumlah tanda baca), dan segala sesuatu di luar daftar itu otomatis ditolak.
  3. Baris 14-17 (pola.fullmatch(komentar)): method fullmatch() mengharuskan seluruh string cocok dengan pola. Tanda < dan > tidak ada dalam allowlist, sehingga komentar ditolak tanpa perlu menebak variasi serangan.

Memvalidasi Tipe, Panjang, dan Rentang #

Validasi klasik memeriksa tipe data, panjang, dan rentang nilai. Kita perlu berhati-hati dengan satu jebakan Python: bool adalah subclass dari int, sehingga isinstance(True, int) bernilai True:

python
def validasi_usia(usia):
    if not isinstance(usia, int) or isinstance(usia, bool):
        return "ditolak: bukan angka bulat"
    if not 13 <= usia <= 120:
        return "ditolak: di luar rentang 13-120"
    return "diterima"


print(validasi_usia(25))
print(validasi_usia("dua puluh lima"))
print(validasi_usia(True))
print(validasi_usia(200))

Output yang Dihasilkan:

text
diterima
ditolak: bukan angka bulat
ditolak: bukan angka bulat
ditolak: di luar rentang 13-120

Pemeriksaan isinstance(usia, bool) pada baris ketiga menutup celah nilai True yang lolos sebagai angka 1. Untuk data berbentuk objek kompleks, library pydantic menjalankan validasi yang sama lewat deklarasi tipe — dan error-nya tercatat rapi:

python
from pydantic import BaseModel, Field, ValidationError


class Pendaftaran(BaseModel):
    nama: str = Field(min_length=2, max_length=50)
    usia: int = Field(ge=13, le=120)


print(Pendaftaran(nama="Budi", usia=20))
try:
    Pendaftaran(nama="B", usia=20)
except ValidationError as e:
    print(e)

Output yang Dihasilkan:

text
nama='Budi' usia=20
1 validation error for Pendaftaran
nama
  String should have at least 2 characters [type=string_too_short, input_value='B', input_type=str]
    For further information visit https://errors.pydantic.dev/2.12/v/string_too_short

Dekorator Field(min_length=2, max_length=50) dan Field(ge=13, le=120) memindahkan aturan validasi ke satu tempat yang terbaca sekilas; pydantic menolak data yang melanggar sebelum sempat menyentuh logika aplikasi. FastAPI memakai mekanisme ini untuk memvalidasi seluruh body request secara otomatis.

Validasi Upload File dan Argumen Perintah #

File upload perlu tiga pemeriksaan: ekstensi (allowlist), ukuran, dan idealnya isi. Ekstensi dibaca dengan Path(nama).suffix lalu dicocokkan ke set allowlist:

python
from pathlib import Path

EKSTENSI_BOLEH = {".jpg", ".jpeg", ".png"}
BATAS_BYTES = 2 * 1024 * 1024


def periksa_upload(nama_file, ukuran_bytes):
    ekstensi = Path(nama_file).suffix.lower()
    if ekstensi not in EKSTENSI_BOLEH:
        return f"{nama_file}: ditolak (ekstensi {ekstensi} tidak diizinkan)"
    if ukuran_bytes > BATAS_BYTES:
        return f"{nama_file}: ditolak (ukuran melebihi 2 MB)"
    return f"{nama_file}: diterima"


print(periksa_upload("foto-budi.png", 800_000))
print(periksa_upload("shell.php", 4_000))
print(periksa_upload("arsip.png", 3_000_000))

Output yang Dihasilkan:

text
foto-budi.png: diterima
shell.php: ditolak (ekstensi .php tidak diizinkan)
arsip.png: ditolak (ukuran melebihi 2 MB)

Pola yang sama berlaku untuk input yang akan diteruskan ke perintah shell — topik yang diperkenalkan pada Chapter subprocess. Nama kolom penyortiran, nama file, dan flag harus dicocokkan ke allowlist sebelum menyentuh subprocess.run():

python
KOLOM_BOLEH = {"nama", "harga", "stok"}


def validasi_kolom(kolom):
    if kolom not in KOLOM_BOLEH:
        raise ValueError(f"kolom tidak dikenal: {kolom!r}")
    return kolom


for kolom in ["harga", "harga; rm -rf /"]:
    try:
        validasi_kolom(kolom)
        print(f"{kolom!r}: aman dipakai")
    except ValueError as e:
        print(f"{kolom!r}: DITOLAK -> ValueError({e})")

Output yang Dihasilkan:

text
'harga': aman dipakai
'harga; rm -rf /': DITOLAK -> ValueError(kolom tidak dikenal: 'harga; rm -rf /')

Input harga; rm -rf / adalah upaya command injection; allowlist menolaknya karena string tersebut memang bukan nama kolom yang sah.

Encoding Output untuk Konteks #

Data yang sudah tersimpan tetap perlu di-escape saat ditampilkan. Isi yang aman di konteks JSON bisa berbahaya di konteks HTML, sehingga escaping mengikuti tujuan akhirnya. Untuk HTML, module html menyediakan html.escape():

python
import html

nama = "<script>alert('XSS')</script>"
print("Tampilan mentah :", nama)
print("Setelah escape  :", html.escape(nama))

Output yang Dihasilkan:

text
Tampilan mentah : <script>alert('XSS')</script>
Setelah escape  : &lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;

Fungsi html.escape() mengubah <, >, &, serta tanda kutip menjadi entity HTML, sehingga browser menampilkannya sebagai teks biasa alih-alih mengeksekusinya sebagai tag. Dengan validasi di pintu masuk dan escaping di pintu keluar, kita sudah menegakkan dua lapis pertahanan — selanjutnya kita amankan data yang paling sensitif di dalam kode: secrets.