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
func validateUsernameBlacklist(username string) bool {
forbiddenChars := []string{"<", ">", "\"", "'", ";", "--", "DROP", "SELECT"}
lower := strings.ToLower(username)
for _, forbidden := range forbiddenChars {
if strings.Contains(lower, strings.ToLower(forbidden)) {
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
func validateUsernameWhitelist(username string) bool {
// Hanya huruf, angka, underscore, dan dash
// Panjang 3-30 karakter
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]{3,30}$`, username)
return matched
}
// 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 #
// CreateUserRequest — aturan validasi
type CreateUserRequest struct {
Username string
Email string
DisplayName string
Bio string
Website string
}
// Validate memeriksa setiap aturan secara manual (stdlib saja)
func (req *CreateUserRequest) Validate() error {
// Panjang minimum dan maksimum
if len(req.Username) < 3 || len(req.Username) > 30 {
return errors.New("username must be 3-30 characters")
}
if len(req.Email) > 254 { // RFC 5321 limit
return errors.New("email too long")
}
if len(req.DisplayName) < 1 || len(req.DisplayName) > 100 {
return errors.New("display_name must be 1-100 characters")
}
if len(req.Bio) > 500 {
return errors.New("bio too long")
}
if len(req.Website) > 2000 {
return errors.New("website too long")
}
// Username: hanya huruf, angka, underscore, dan dash
if !regexp.MustCompile(`^[a-zA-Z0-9_-]+$`).MatchString(req.Username) {
return errors.New("username may only contain letters, numbers, underscores, and dashes")
}
// Email: validasi format dasar — gunakan library untuk validasi lengkap
if !regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`).MatchString(req.Email) {
return errors.New("invalid email format")
}
req.Email = strings.ToLower(req.Email) // normalisasi: lowercase email
// Website: harus http atau https dan memiliki host
parsed, err := url.Parse(req.Website)
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
return errors.New("website must use http or https")
}
return nil
}
// Penggunaan di endpoint
func createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
return
}
if err := req.Validate(); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
// Data sudah tervalidasi — aman diproses
user := User.Create(req.Username, req.Email, req.DisplayName)
writeJSON(w, http.StatusCreated, map[string]string{"user_id": user.ID})
}
Angka: Range, Tipe, dan Batasan Bisnis #
// TransferRequest — validasi uang
type TransferRequest struct {
ToAccount string
Amount float64
Description string
}
func (req *TransferRequest) Validate() error {
// Nomor rekening: hanya angka, 10-20 karakter
if len(req.ToAccount) < 10 || len(req.ToAccount) > 20 {
return errors.New("to_account must be 10-20 characters")
}
for _, c := range req.ToAccount {
if c < '0' || c > '9' {
return errors.New("account numbers may only contain digits")
}
}
// Amount: > 0 dan <= 100 juta, maksimal 2 desimal
if req.Amount <= 0 || req.Amount > 100_000_000 {
return errors.New("amount must be > 0 and <= 100 million")
}
if math.Round(req.Amount*100) != req.Amount*100 {
return errors.New("transfer amounts may have at most 2 decimals")
}
return nil
}
// Validasi yang sering terlewat untuk angka:
type ProductRequest struct {
Price float64
}
func (req *ProductRequest) Validate() error {
if req.Price < 0 {
return errors.New("price must not be negative")
}
const maxPrice = 1_000_000_000 // 1 miliar
if req.Price > maxPrice {
return fmt.Errorf("price exceeds the maximum limit of %d", maxPrice)
}
return nil
}
// Pagination — mencegah per_page=999999 yang menyebabkan server load tinggi
type PaginationRequest struct {
Page int // >= 1
PerPage int // 1-100
}
func (req *PaginationRequest) Validate() error {
if req.Page < 1 {
return errors.New("page must be >= 1")
}
if req.PerPage < 1 || req.PerPage > 100 {
return errors.New("per_page must be 1-100")
}
return nil
}
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 (net/mail adalah bagian dari stdlib)
import "net/mail"
func validateAndNormalizeEmail(email string) (string, error) {
// mail.ParseAddress melakukan:
// - Format check (ada @, domain valid, dll)
// - Normalisasi (bagian address diparse secara struktural)
parsed, err := mail.ParseAddress(email)
if err != nil {
return "", fmt.Errorf("invalid email: %w", err)
}
// Normalisasi: lowercase bagian domain
parts := strings.SplitN(parsed.Address, "@", 2)
return strings.ToLower(parts[0]) + "@" + strings.ToLower(parts[1]), nil
}
// Contoh:
// "[email protected]" → "[email protected]"
// "[email protected]" → "[email protected]" (valid)
// "not-an-email" → error
// "@nodomain" → error
File Upload: Validasi yang Komprehensif #
// Whitelist MIME types yang diizinkan (extension per MIME type)
var allowedImageTypes = map[string][]string{
"image/jpeg": {".jpg", ".jpeg"},
"image/png": {".png"},
"image/gif": {".gif"},
"image/webp": {".webp"},
}
var allowedDocumentTypes = map[string][]string{
"application/pdf": {".pdf"},
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {".docx"},
}
const maxFileSize = 10 * 1024 * 1024 // 10MB
// validateFileUpload memvalidasi file upload secara komprehensif.
// Returns: (valid bool, mimeType string, errors []string)
func validateFileUpload(header *multipart.FileHeader, allowedTypes map[string][]string) (bool, string, []string) {
var errors []string
// 1. Cek ukuran file
if header.Size == 0 {
errors = append(errors, "File must not be empty")
} else if header.Size > maxFileSize {
errors = append(errors, fmt.Sprintf("Maximum file size is %dMB", maxFileSize/1024/1024))
}
// 2. Deteksi MIME type dari KONTEN file, bukan extension
// atau Content-Type header (keduanya bisa dipalsukan oleh attacker)
f, err := header.Open()
if err != nil {
return false, "", append(errors, "Cannot read file")
}
defer f.Close()
headerBytes := make([]byte, 2048)
n, _ := f.Read(headerBytes)
detectedMime := http.DetectContentType(headerBytes[:n])
if _, ok := allowedTypes[detectedMime]; !ok {
return false, "", append(errors,
"File type not allowed. Detected as: "+detectedMime+
". Allowed types: "+fmt.Sprint(maps.Keys(allowedTypes)))
}
// 3. Validasi extension (opsional tapi berguna)
if filename := header.Filename; filename != "" {
ext := strings.ToLower(path.Ext(filename))
allowedExtensions := allowedTypes[detectedMime]
if !slices.Contains(allowedExtensions, ext) {
errors = append(errors, "Extension '"+ext+"' doesn't match the detected file type")
}
}
return len(errors) == 0, detectedMime, errors
}
Validasi JSON Schema #
Untuk API yang menerima JSON, schema validation memastikan struktur dan tipe data sesuai ekspektasi sebelum diproses.
// Dengan Go — struct yang bertipe memberikan validasi schema di level JSON
type OrderItemRequest struct {
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
Notes string `json:"notes,omitempty"`
}
type CreateOrderRequest struct {
Items []OrderItemRequest `json:"items"`
ShippingAddressID int `json:"shipping_address_id"`
UsePoints bool `json:"use_points"`
PromoCode string `json:"promo_code,omitempty"`
}
// Validasi aturan manual di atas schema yang bertipe
func (req *CreateOrderRequest) Validate() error {
// 1 sampai 100 item
if len(req.Items) < 1 || len(req.Items) > 50 {
return errors.New("items must contain 1-50 entries")
}
if req.ShippingAddressID <= 0 {
return errors.New("shipping_address_id must be > 0")
}
// Promo code hanya huruf besar dan angka
if req.PromoCode != "" && !regexp.MustCompile(`^[A-Z0-9]{3,20}$`).MatchString(req.PromoCode) {
return errors.New("invalid promo code format")
}
// Tidak boleh ada produk duplikat dalam satu order
seen := map[int]bool{}
for _, item := range req.Items {
if item.ProductID <= 0 {
return errors.New("product_id must be > 0")
}
if item.Quantity < 1 || item.Quantity > 100 {
return errors.New("quantity must be 1-100")
}
if seen[item.ProductID] {
return errors.New("no duplicate products allowed in one order")
}
seen[item.ProductID] = true
}
return nil
}
// PENTING: tolak field yang tidak dikenal
// decoder.DisallowUnknownFields() membuat field tak dikenal menjadi error decode
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
func validateRequiredString(value string) (string, error) {
if value == "" {
return "", errors.New("field is required")
}
stripped := strings.TrimSpace(value)
if stripped == "" {
return "", errors.New("field must not be only whitespace")
}
return stripped, nil
}
// Edge case 2: angka dalam string vs angka
// API menerima {"quantity": "10"} padahal expected integer
// Decode ke int — "10" (string) ditolak oleh encoding/json
type OrderItem struct {
Quantity int `json:"quantity"`
// json.Decoder.UseNumber + pengetikan ketat menolak nilai string
}
// Edge case 3: array/list yang terlalu panjang — DoS vector
type SearchRequest struct {
Tags []string `json:"tags"`
// Tanpa batas: user bisa kirim 10.000 tags → server overload
}
func (req *SearchRequest) Validate() error {
if len(req.Tags) > 20 {
return errors.New("too many tags (max 20)")
}
return nil
}
// Edge case 4: nested object depth yang tidak terbatas
// JSON {"a":{"b":{"c":{"d":{"e":{...}}}}}} ribuan level
// Menyebabkan stack overflow di beberapa parser
func validateJSONDepth(data any, maxDepth, currentDepth int) error {
if currentDepth > maxDepth {
return fmt.Errorf("JSON structure too deep (max %d levels)", maxDepth)
}
switch v := data.(type) {
case map[string]any:
for _, value := range v {
if err := validateJSONDepth(value, maxDepth, currentDepth+1); err != nil {
return err
}
}
case []any:
for _, item := range v {
if err := validateJSONDepth(item, maxDepth, currentDepth+1); err != nil {
return err
}
}
}
return nil
}
// Edge case 5: integer overflow
// Platform lama: 32-bit integer max = 2,147,483,647
// Jika user kirim quantity = 2147483648 → overflow bug
type QuantityRequest struct {
Quantity int `json:"quantity"` // dengan batas atas eksplisit:
}
func (req *QuantityRequest) Validate() error {
if req.Quantity < 1 || req.Quantity > 2_147_483_647 { // 32-bit int max
return errors.New("quantity out of range")
}
return nil
}
// Edge case 6: path traversal di nama file
func validateFilename(filename string) (string, error) {
// Ambil hanya nama file, tanpa path
safeName := path.Base(filepath.ToSlash(filename))
if safeName == "" || strings.HasPrefix(safeName, ".") {
return "", errors.New("invalid filename")
}
// Hapus karakter berbahaya
re := regexp.MustCompile(`[^\w\-.]`)
safeName = re.ReplaceAllString(safeName, "_")
return safeName, nil
}
// validateFilename("../../etc/passwd") → "passwd"
// validateFilename(".htaccess") → error
// validateFilename("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": "Validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format. Example: [email protected]"
},
{
"field": "username",
"message": "Username may only contain letters, numbers, underscores, and dashes"
}
]
}
// Implementasi error response yang konsisten
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func validationErrorResponse(errors []FieldError) (int, map[string]any) {
// Standarisasi respons error validasi.
return 400, map[string]any{
"error": "Validation failed",
"details": errors,
}
}
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
func sanitizeInput(value string) string {
return strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(value, "'", "''"), ";", ""), "--", "")
}
// 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
func search(w http.ResponseWriter, r *http.Request) {
query := r.FormValue("query")
// Tidak ada batasan panjang query
// Attacker bisa kirim string 100MB sebagai query
results := searchDB(query)
}
// ✓ Solusi: selalu batasi panjang input
// ✗ Anti-pattern 4: mempercayai Content-Type header untuk file type
func uploadFile(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type") // bisa dipalsukan!
if strings.HasPrefix(contentType, "image/jpeg") {
saveAsImage(r)
}
}
// ✓ 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
type UserProfile struct {
Name string
Bio string // bio tidak divalidasi! user bisa kirim bio 10MB
}
// ✓ Solusi: field opsional tetap perlu batas
type UserProfile struct {
Name string // min 1, max 100
Bio string // max 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 →