Input Validation #
Setiap data yang masuk ke sistem dari luar — dari form, dari URL parameter, dari API request body, dari header HTTP, dari file yang diupload — harus dianggap tidak aman sampai terbukti sebaliknya. Ini bukan tentang paranoia; ini tentang kenyataan bahwa sistem tidak bisa mengontrol apa yang dikirimkan oleh client.
Browser yang baik mengirimkan data yang valid. Browser yang dikontrol attacker, tool seperti Burp Suite atau curl, atau script otomatis bisa mengirimkan apapun — payload SQL injection, skrip XSS, file yang menyamar sebagai gambar, ukuran yang melampaui ekspektasi, atau tipe data yang berbeda dari yang diharapkan. Sistem yang tidak memvalidasi input dengan ketat adalah sistem yang mempercayai semua ini secara buta.
Input validation bukan hanya tentang keamanan. Ia tentang integritas sistem: memastikan bahwa data yang masuk ke database, yang diproses oleh business logic, dan yang dikembalikan ke pengguna lain adalah data yang valid, konsisten, dan tidak akan merusak sistem.
Mengapa Validasi di Server Adalah Wajib #
Validasi di client-side (JavaScript) memberikan UX yang baik — feedback instan saat user mengisi form. Tapi ia tidak bisa diandalkan untuk keamanan.
Kenapa client-side validation tidak cukup:
1. Bisa dibypass sepenuhnya:
curl -X POST https://api.contoh.com/users \
-H "Content-Type: application/json" \
-d '{"email": "bukan-email", "age": -999}'
→ Tidak ada JavaScript yang berjalan, validasi frontend diabaikan
2. Browser developer tools:
Attacker bisa edit JavaScript di browser untuk skip validasi
atau mengirim request langsung dari Network tab
3. Automated tools:
SQLMap, Burp Suite, dan tool fuzzing lainnya mengirim
ribuan request dengan berbagai payload tanpa melalui UI
4. Mobile app dan third-party client:
API yang diakses dari mobile app atau third-party
tidak melalui frontend yang sama sekali
Prinsip: client-side validation untuk UX,
server-side validation untuk keamanan dan integritas
flowchart LR
subgraph Client
A[Browser/App] --> B[Validasi Client\nuntuk UX]
B --> C[Kirim Request]
end
subgraph Server["Server — Validasi Wajib"]
D[Terima Request] --> E[Validasi Input]
E --> |Invalid| F[400 Bad Request\nPesan jelas]
E --> |Valid| G[Sanitasi/Encoding]
G --> H[Business Logic]
H --> I[Database/Storage]
end
C --> DWhitelist vs Blacklist: Prinsip Dasar #
Dua pendekatan dalam validasi: whitelist (izinkan yang eksplisit diketahui aman) dan blacklist (tolak yang diketahui berbahaya). Whitelist selalu lebih aman.
# ANTI-PATTERN: blacklist — mencoba melarang yang diketahui berbahaya
def validate_username_blacklist(username: str) -> bool:
forbidden_chars = ['<', '>', '"', "'", ';', '--', 'DROP', 'SELECT']
for forbidden in forbidden_chars:
if forbidden.lower() in username.lower():
return False
return True
# Masalah dengan blacklist:
# → Tidak pernah lengkap — selalu ada teknik bypass
# → Attacker menggunakan encoding: %3C untuk <, < untuk <, \u003c untuk <
# → Attacker menggunakan case variation, Unicode tricks, dll
# BENAR: whitelist — hanya izinkan yang diketahui aman
import re
def validate_username_whitelist(username: str) -> bool:
# Hanya huruf, angka, underscore, dan dash
# Panjang 3-30 karakter
pattern = r'^[a-zA-Z0-9_-]{3,30}$'
return bool(re.match(pattern, username))
# Jika tidak cocok dengan pattern → ditolak
# Tidak peduli teknik bypass apapun yang digunakan
Whitelist vs Blacklist dalam berbagai konteks:
Username:
✓ Whitelist: [a-zA-Z0-9_-], 3-30 karakter
✗ Blacklist: larang <, >, ;, dll (mudah dibypass)
Tipe file upload:
✓ Whitelist: hanya jpg, png, gif, pdf
✗ Blacklist: larang exe, php, js (bisa pakai .phtml, .php5, dll)
HTML di rich text editor:
✓ Whitelist: hanya <b>, <i>, <p>, <a href="..."> dengan attribute terbatas
✗ Blacklist: larang <script>, onerror= (terlalu banyak cara bypass)
URL redirect:
✓ Whitelist: hanya domain yang terdaftar
✗ Blacklist: larang javascript:, data: (ada banyak encoding lain)
Validasi Per Tipe Data #
String: Panjang, Format, dan Karakter #
from pydantic import BaseModel, validator, Field
import re
from typing import Optional
class CreateUserRequest(BaseModel):
# Panjang minimum dan maksimum
username: str = Field(min_length=3, max_length=30)
email: str = Field(max_length=254) # RFC 5321 limit
display_name: str = Field(min_length=1, max_length=100)
bio: Optional[str] = Field(default=None, max_length=500)
website: Optional[str] = Field(default=None, max_length=2000)
@validator('username')
def username_alphanumeric(cls, v):
if not re.match(r'^[a-zA-Z0-9_-]+$', v):
raise ValueError(
'Username hanya boleh mengandung huruf, angka, underscore, dan dash'
)
return v
@validator('email')
def email_valid_format(cls, v):
# Validasi format dasar — gunakan library untuk validasi lengkap
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', v):
raise ValueError('Format email tidak valid')
# Normalisasi: lowercase email
return v.lower()
@validator('website')
def website_safe_url(cls, v):
if v is None:
return v
from urllib.parse import urlparse
parsed = urlparse(v)
if parsed.scheme not in ('http', 'https'):
raise ValueError('Website harus menggunakan http atau https')
if not parsed.netloc:
raise ValueError('URL website tidak valid')
return v
# Penggunaan di endpoint Flask
@app.route('/users', methods=['POST'])
def create_user():
try:
data = CreateUserRequest(**request.json)
except ValidationError as e:
return jsonify({'errors': e.errors()}), 400
# Data sudah tervalidasi — aman diproses
user = User.create(
username=data.username,
email=data.email,
display_name=data.display_name
)
return jsonify({'user_id': user.id}), 201
Angka: Range, Tipe, dan Batasan Bisnis #
class TransferRequest(BaseModel):
to_account: str = Field(min_length=10, max_length=20)
amount: float = Field(gt=0, le=100_000_000) # > 0 dan <= 100 juta
description: Optional[str] = Field(default=None, max_length=200)
@validator('amount')
def amount_valid_decimal(cls, v):
# Hanya izinkan 2 desimal untuk mata uang
if round(v, 2) != v:
raise ValueError('Jumlah transfer maksimal 2 desimal')
return v
@validator('to_account')
def account_number_digits_only(cls, v):
if not v.isdigit():
raise ValueError('Nomor rekening hanya boleh berisi angka')
return v
# Validasi yang sering terlewat untuk angka:
class ProductRequest(BaseModel):
price: float
@validator('price')
def price_not_negative(cls, v):
if v < 0:
raise ValueError('Harga tidak boleh negatif')
return v
# Validasi bisnis: harga produk tidak mungkin di atas threshold tertentu
@validator('price')
def price_reasonable(cls, v):
MAX_PRICE = 1_000_000_000 # 1 miliar
if v > MAX_PRICE:
raise ValueError(f'Harga melebihi batas maksimum {MAX_PRICE}')
return v
class PaginationRequest(BaseModel):
page: int = Field(ge=1, default=1) # >= 1
per_page: int = Field(ge=1, le=100, default=20) # 1-100
# Mencegah: per_page=999999 yang menyebabkan server load tinggi
Email: Validasi yang Benar #
# ANTI-PATTERN: regex email yang terlalu sederhana atau terlalu kompleks
# Regex email yang "sempurna" sangat panjang dan susah dibaca
# Bahkan RFC 5322 email validation yang lengkap tidak practical
# BENAR: gunakan library yang sudah teruji
# pip install email-validator
from email_validator import validate_email, EmailNotValidError
def validate_and_normalize_email(email: str) -> str:
try:
# validate_email melakukan:
# - Format check (ada @, domain valid, dll)
# - DNS check opsional (cek apakah domain punya MX record)
# - Normalisasi (lowercase, handle unicode)
valid = validate_email(email, check_deliverability=False)
return valid.email # email yang sudah dinormalisasi
except EmailNotValidError as e:
raise ValueError(f"Email tidak valid: {e}")
# Contoh:
# "[email protected]" → "[email protected]"
# "[email protected]" → "[email protected]" (valid)
# "not-an-email" → ValueError
# "@nodomain" → ValueError
File Upload: Validasi yang Komprehensif #
import magic
import hashlib
from pathlib import Path
# Whitelist MIME types yang diizinkan
ALLOWED_IMAGE_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'image/webp': ['.webp'],
}
ALLOWED_DOCUMENT_TYPES = {
'application/pdf': ['.pdf'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
def validate_file_upload(file, allowed_types: dict) -> dict:
"""
Validasi file upload secara komprehensif.
Returns: {'valid': bool, 'mime_type': str, 'errors': list}
"""
errors = []
# 1. Cek ukuran file
file.seek(0, 2) # seek ke akhir
file_size = file.tell()
file.seek(0) # kembali ke awal
if file_size == 0:
errors.append("File tidak boleh kosong")
elif file_size > MAX_FILE_SIZE:
errors.append(f"Ukuran file maksimal {MAX_FILE_SIZE // 1024 // 1024}MB")
if errors:
return {'valid': False, 'errors': errors}
# 2. Deteksi MIME type dari KONTEN file, bukan extension atau Content-Type header
# (keduanya bisa dipalsukan oleh attacker)
header_bytes = file.read(2048)
file.seek(0)
detected_mime = magic.from_buffer(header_bytes, mime=True)
if detected_mime not in allowed_types:
errors.append(
f"Tipe file tidak diizinkan. "
f"Dideteksi sebagai: {detected_mime}. "
f"Tipe yang diizinkan: {list(allowed_types.keys())}"
)
return {'valid': False, 'errors': errors}
# 3. Validasi extension (opsional tapi berguna)
filename = getattr(file, 'filename', '')
if filename:
ext = Path(filename).suffix.lower()
allowed_extensions = allowed_types.get(detected_mime, [])
if ext not in allowed_extensions:
errors.append(
f"Extension '{ext}' tidak sesuai dengan tipe file yang terdeteksi"
)
# 4. Untuk gambar: validasi konten lebih dalam
if detected_mime.startswith('image/'):
try:
from PIL import Image
file.seek(0)
img = Image.open(file)
img.verify() # raise exception jika file corrupt/manipulated
file.seek(0)
# Cek dimensi yang wajar
width, height = img.size
if width > 10000 or height > 10000:
errors.append("Dimensi gambar terlalu besar")
except Exception as e:
errors.append(f"File gambar tidak valid: {str(e)}")
if errors:
return {'valid': False, 'errors': errors}
return {'valid': True, 'mime_type': detected_mime, 'errors': []}
Validasi JSON Schema #
Untuk API yang menerima JSON, schema validation memastikan struktur dan tipe data sesuai ekspektasi sebelum diproses.
# Dengan Pydantic (Python) — pendekatan yang paling clean
from pydantic import BaseModel, Field, validator
from typing import List, Optional
from enum import Enum
from datetime import date
class OrderStatus(str, Enum):
pending = "pending"
processing = "processing"
shipped = "shipped"
delivered = "delivered"
class OrderItemRequest(BaseModel):
product_id: int = Field(gt=0)
quantity: int = Field(ge=1, le=100) # 1 sampai 100 item
notes: Optional[str] = Field(default=None, max_length=200)
class CreateOrderRequest(BaseModel):
items: List[OrderItemRequest] = Field(min_items=1, max_items=50)
shipping_address_id: int = Field(gt=0)
use_points: bool = False
promo_code: Optional[str] = Field(default=None, max_length=20)
@validator('promo_code')
def promo_code_format(cls, v):
if v is not None:
# Promo code hanya huruf besar dan angka
if not re.match(r'^[A-Z0-9]{3,20}$', v):
raise ValueError('Format promo code tidak valid')
return v
@validator('items')
def no_duplicate_products(cls, items):
product_ids = [item.product_id for item in items]
if len(product_ids) != len(set(product_ids)):
raise ValueError('Tidak boleh ada produk duplikat dalam satu order')
return items
# Atau dengan jsonschema untuk validasi yang lebih lightweight
import jsonschema
CREATE_ORDER_SCHEMA = {
"type": "object",
"required": ["items", "shipping_address_id"],
"properties": {
"items": {
"type": "array",
"minItems": 1,
"maxItems": 50,
"items": {
"type": "object",
"required": ["product_id", "quantity"],
"properties": {
"product_id": {"type": "integer", "minimum": 1},
"quantity": {"type": "integer", "minimum": 1, "maximum": 100},
}
}
},
"shipping_address_id": {"type": "integer", "minimum": 1},
"promo_code": {
"type": "string",
"pattern": "^[A-Z0-9]{3,20}$"
}
},
"additionalProperties": False # PENTING: tolak field yang tidak dikenal
}
def validate_json_schema(data: dict, schema: dict):
try:
jsonschema.validate(instance=data, schema=schema)
except jsonschema.ValidationError as e:
raise ValueError(f"Invalid request: {e.message}")
Sanitasi vs Encoding vs Validation: Perbedaan yang Penting #
Tiga konsep ini sering digunakan secara bergantian padahal berbeda.
Validation — apakah input memenuhi aturan?
→ Keputusan: terima atau tolak
→ Dilakukan sebelum memproses
→ Contoh: apakah ini email yang valid?
Sanitasi — bersihkan input dari konten berbahaya
→ Ubah input: hapus atau escape elemen berbahaya
→ Gunakan untuk rich text (HTML dengan tag terbatas)
→ Contoh: hapus <script> dari HTML yang diinput user
Encoding/Escaping — ubah karakter khusus agar aman di konteks tertentu
→ Dilakukan saat OUTPUT, bukan input
→ Konteks berbeda butuh encoding berbeda
→ Contoh: & → & saat render ke HTML
Urutan yang benar:
1. Validate input (tolak jika tidak valid)
2. Proses dan simpan (gunakan parameterized query, jangan concat)
3. Sanitasi jika diperlukan (untuk rich text)
4. Encode saat output (sesuai konteks render)
Kesalahan umum:
✗ Sanitasi input yang bertujuan untuk mencegah SQL injection
→ Gunakan parameterized query, bukan sanitasi input
✗ Encode input sebelum disimpan ke database
→ Encode saat output, bukan saat simpan
→ Data di database seharusnya dalam bentuk aslinya
Edge Case yang Sering Terlewat #
# Edge case 1: string kosong vs None vs whitespace
def validate_required_string(value: str) -> str:
if value is None:
raise ValueError("Field wajib diisi")
stripped = value.strip()
if not stripped:
raise ValueError("Field tidak boleh hanya berisi spasi")
return stripped
# Edge case 2: angka dalam string vs angka
# API menerima {"quantity": "10"} padahal expected integer
# Pydantic menangani ini, tapi pastikan tipe di-enforce
class OrderItem(BaseModel):
quantity: int # Pydantic akan error jika "10" (string) diterima sebagai quantity
# Atau konfigurasi untuk strict mode:
class Config:
# Strict mode: tidak ada coercion, tipe harus exact match
# "10" tidak akan otomatis dikonversi ke 10
strict = True
# Edge case 3: array/list yang terlalu panjang — DoS vector
class SearchRequest(BaseModel):
tags: List[str] = Field(default=[], max_items=20)
# Tanpa max_items: user bisa kirim 10.000 tags → server overload
# Edge case 4: nested object depth yang tidak terbatas
# JSON {"a": {"b": {"c": {"d": {"e": {...}}}}}} ribuan level
# Menyebabkan stack overflow di beberapa parser
def validate_json_depth(data, max_depth: int = 10, current_depth: int = 0):
if current_depth > max_depth:
raise ValueError(f"Struktur JSON terlalu dalam (max {max_depth} level)")
if isinstance(data, dict):
for value in data.values():
validate_json_depth(value, max_depth, current_depth + 1)
elif isinstance(data, list):
for item in data:
validate_json_depth(item, max_depth, current_depth + 1)
# Edge case 5: integer overflow
# Platform lama: 32-bit integer max = 2,147,483,647
# Jika user kirim quantity = 2147483648 → overflow bug
class QuantityRequest(BaseModel):
quantity: int = Field(ge=1, le=2_147_483_647) # 32-bit int max
# Edge case 6: path traversal di nama file
def validate_filename(filename: str) -> str:
# Hapus directory traversal attempts
from pathlib import PurePosixPath
# Ambil hanya nama file, tanpa path
safe_name = PurePosixPath(filename).name
if not safe_name or safe_name.startswith('.'):
raise ValueError("Nama file tidak valid")
# Hapus karakter berbahaya
safe_name = re.sub(r'[^\w\-.]', '_', safe_name)
return safe_name
# validate_filename("../../etc/passwd") → "passwd"
# validate_filename(".htaccess") → ValueError
# validate_filename("file<>name.pdf") → "file__name.pdf"
Pesan Error yang Berguna tapi Tidak Bocor Informasi #
# ANTI-PATTERN: pesan error yang terlalu verbose
# → Membocorkan informasi sistem ke attacker
{
"error": "Column 'email' cannot be null in table 'users'",
"sql": "INSERT INTO users (username, email) VALUES (?, NULL)"
}
# ANTI-PATTERN: pesan error yang terlalu generik
# → User tidak tahu apa yang harus diperbaiki
{"error": "Invalid request"}
# BENAR: pesan error yang informatif untuk user tapi tidak bocorkan internals
# Response 400 dengan detail yang berguna
{
"error": "Validasi gagal",
"details": [
{
"field": "email",
"message": "Format email tidak valid. Contoh: [email protected]"
},
{
"field": "username",
"message": "Username hanya boleh mengandung huruf, angka, underscore, dan dash"
}
]
}
# Implementasi error response yang konsisten
def validation_error_response(errors: list) -> tuple:
"""Standardize validation error response."""
return jsonify({
'error': 'Validation failed',
'details': [
{
'field': error.get('loc', ['unknown'])[-1],
'message': error['msg']
}
for error in errors
]
}), 400
Validasi di Beberapa Lapisan #
Validasi yang efektif terjadi di beberapa lapisan, masing-masing dengan tanggung jawab berbeda:
Lapisan 1 — API/Controller Layer:
→ Validasi format dan tipe (adalah ini email? apakah ini integer?)
→ Schema validation (semua field required ada?)
→ Batas ukuran (panjang string, jumlah item, ukuran file)
→ Ini adalah "pintu masuk" — tolak sebelum masuk ke business logic
Lapisan 2 — Business Logic Layer:
→ Validasi aturan bisnis (stok cukup? user punya saldo? promo code valid?)
→ Validasi relasi antar field (tanggal mulai < tanggal akhir)
→ Validasi kontekstual (user boleh melakukan operasi ini?)
Lapisan 3 — Database Layer (last resort):
→ Constraint NOT NULL, UNIQUE, FOREIGN KEY
→ CHECK constraint untuk range nilai
→ Type enforcement (kolom integer tidak menerima string)
→ Ini safety net — seharusnya tidak sering diperlukan jika lapisan atas bekerja
Prinsip: semakin awal error terdeteksi, semakin murah biayanya.
Anti-Pattern yang Harus Dihindari #
# ✗ Anti-pattern 1: mengandalkan client-side validation saja
# Tidak ada validasi di server — hanya validasi JavaScript di browser
# curl langsung ke API: tidak ada validation sama sekali
# ✗ Anti-pattern 2: blacklist untuk SQL injection prevention
def sanitize_input(value):
return value.replace("'", "''").replace(";", "").replace("--", "")
# Tidak lengkap, mudah dibypass, dan bukan solusi yang tepat
# ✓ Solusi: parameterized query — tidak perlu sanitasi untuk SQL
# ✗ Anti-pattern 3: tidak ada batas ukuran input
@app.route('/search', methods=['POST'])
def search():
query = request.json.get('query', '')
# Tidak ada batasan panjang query
# Attacker bisa kirim string 100MB sebagai query
results = search_db(query)
# ✓ Solusi: selalu batasi panjang input
# ✗ Anti-pattern 4: mempercayai Content-Type header untuk file type
def upload_file():
content_type = request.files['file'].content_type # bisa dipalsukan!
if content_type == 'image/jpeg':
save_as_image(request.files['file'])
# ✓ Solusi: deteksi MIME type dari konten file (magic bytes)
# ✗ Anti-pattern 5: menyimpan data yang sudah di-encode ke database
# User mengirim: <script>alert(1)</script>
# Validasi: konversi ke <script>alert(1)</script>
# Simpan ke database: <script>alert(1)</script> ← salah!
# Saat ditampilkan: double-encode, data rusak
# ✓ Solusi: simpan data asli di database, encode saat output
# ✗ Anti-pattern 6: tidak ada validasi pada field opsional
class UserProfile(BaseModel):
name: str
bio: Optional[str] = None
# bio tidak divalidasi! user bisa kirim bio 10MB
# ✓ Solusi: field opsional tetap perlu batas
class UserProfile(BaseModel):
name: str = Field(min_length=1, max_length=100)
bio: Optional[str] = Field(default=None, max_length=500)
Checklist Input Validation #
PRINSIP DASAR:
□ Semua validasi dilakukan di server, tidak bergantung pada client
□ Pendekatan whitelist digunakan — hanya izinkan yang diketahui aman
□ Validasi terjadi di lapisan yang tepat (API, business logic, database)
□ Default behavior: tolak input yang tidak dikenal atau tidak sesuai skema
FORMAT & TIPE:
□ Tipe data divalidasi (integer, string, boolean, date, dll)
□ Format divalidasi (email, URL, phone number, UUID, dll)
□ Range nilai divalidasi untuk angka (min, max)
□ Panjang divalidasi untuk string (min_length, max_length)
□ Jumlah item divalidasi untuk array/list (min_items, max_items)
BISNIS:
□ Aturan bisnis divalidasi (stok cukup, saldo cukup, status valid)
□ Relasi antar field divalidasi (date_start < date_end)
□ Constraint unik divalidasi (email belum terdaftar, username available)
FILE UPLOAD:
□ MIME type dideteksi dari konten file, bukan extension atau Content-Type header
□ Ukuran file dibatasi
□ Nama file disanitasi (hapus path traversal, karakter berbahaya)
□ File disimpan di luar web root
□ File gambar divalidasi strukturnya (bukan hanya MIME type)
EDGE CASES:
□ String kosong dan whitespace-only ditangani
□ Null/None ditangani sesuai requirement
□ Path traversal dicegah untuk input yang digunakan sebagai nama file
□ Nested JSON depth dibatasi
□ Integer overflow dicegah dengan batas yang eksplisit
□ Array yang tidak terbatas dicegah dengan max_items
ERROR HANDLING:
□ Pesan error berguna untuk user tapi tidak bocorkan informasi internal
□ Response 400 dengan detail field yang bermasalah
□ Log validation failure untuk monitoring (tapi tidak log data sensitif)
DATABASE:
□ Constraint di database sebagai safety net terakhir
□ NOT NULL untuk field yang wajib
□ CHECK constraint untuk range nilai
□ UNIQUE constraint untuk field yang harus unik
Ringkasan #
- Validasi di server adalah wajib — validasi di client hanya untuk UX — apapun yang dikirim client bisa dipalsukan. Curl, Burp Suite, dan script otomatis tidak melalui validasi frontend.
- Whitelist selalu lebih aman dari blacklist — blacklist tidak pernah lengkap. Ada selalu teknik bypass yang belum masuk daftar. Whitelist mendefinisikan apa yang diizinkan, bukan apa yang dilarang.
- Validasi terjadi di beberapa lapisan — API layer untuk format dan tipe, business logic layer untuk aturan bisnis, database layer sebagai safety net. Semakin awal error terdeteksi, semakin murah.
- Validasi, sanitasi, dan encoding adalah tiga hal berbeda — validasi memutuskan terima atau tolak, sanitasi membersihkan konten berbahaya dari rich text, encoding mengubah karakter khusus saat output. Jangan campur perannya.
- Tipe data harus di-enforce secara ketat — “10” bukan 10. Penggunaan library schema validation seperti Pydantic atau jsonschema memastikan tipe yang benar sebelum data diproses.
- MIME type file harus dideteksi dari konten, bukan extension —
file.jpgbisa berisi PHP,file.pdfbisa berisi executable. Magic bytes tidak bisa dipalsukan di content level.- Field opsional tetap butuh validasi — bio yang None valid, tapi bio yang None tidak sama dengan bio 10MB string. Max_length tetap diperlukan meski field tidak wajib.
- Batasi semua ukuran — panjang string, jumlah item dalam array, ukuran file, dan depth nested JSON. Tanpa batas ini, satu request bisa menguras resource server.
- Simpan data asli di database, encode saat output — encoding di waktu penyimpanan menyebabkan double-encoding dan merusak data. Encode berdasarkan konteks di mana data akan ditampilkan.
- Pesan error yang baik membantu user tanpa membocorkan informasi internal — stack trace, nama tabel database, dan query SQL tidak boleh ada di response error yang dilihat user.
← Sebelumnya: Authorization Berikutnya: Remote Code Execution →