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:
| Aspek | Validasi | Sanitasi |
|---|---|---|
| Pertanyaan | “Apakah data ini sah?” | “Bisakah data ini dibersihkan?” |
| Tindakan saat bermasalah | Tolak seluruh input | Ubah lalu terima |
| Contoh | Field usia harus angka 13-120 | Karakter < diubah menjadi < |
| Kapan dipakai | Data 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>:
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:
Blocklist: komentar diterima (bahaya!)
Allowlist: komentar ditolakMari kita bedah kode di atas:
- Baris 5-6 (
blokir = ...): blocklist hanya mengenal dua bentuk tag. Karakter!di dalam<scr!pt>membuat pencocokan string gagal, sehingga komentar lolos. - 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. - Baris 14-17 (
pola.fullmatch(komentar)): methodfullmatch()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:
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:
diterima
ditolak: bukan angka bulat
ditolak: bukan angka bulat
ditolak: di luar rentang 13-120Pemeriksaan 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:
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:
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_shortDekorator 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:
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:
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():
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:
'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():
import html
nama = "<script>alert('XSS')</script>"
print("Tampilan mentah :", nama)
print("Setelah escape :", html.escape(nama))Output yang Dihasilkan:
Tampilan mentah : <script>alert('XSS')</script>
Setelah escape : <script>alert('XSS')</script>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.