OWASP Cheatsheet #

OWASP Cheat Sheets adalah koleksi panduan praktis yang dikurasi dari pengalaman ribuan engineer keamanan di seluruh dunia. Berbeda dari OWASP Top 10 yang mendeskripsikan risiko, Cheat Sheets memberi panduan implementasi — apa yang harus dilakukan dan bagaimana melakukannya dengan benar.

Artikel ini merangkum cheat sheet yang paling relevan untuk developer backend dan fullstack, dilengkapi dengan contoh kode konkret dan anti-pattern yang perlu dihindari. Ini dirancang sebagai referensi yang bisa dikonsultasikan saat membangun fitur baru, melakukan security review, atau ketika ragu tentang cara yang benar mengimplementasikan sesuatu.

Authentication Cheat Sheet #

Autentikasi yang lemah adalah salah satu penyebab paling umum dari account takeover. Sebagian besar serangan berhasil bukan karena attacker memiliki kemampuan luar biasa, tapi karena sistem tidak memiliki perlindungan dasar.

# Prinsip 1: Hash password dengan algoritma yang tepat
# ANTI-PATTERN: MD5 atau SHA256 tanpa salt — cepat, mudah di-crack
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()

# BENAR: Argon2id (rekomendasi terkuat saat ini)
from argon2 import PasswordHasher
ph = PasswordHasher(
    time_cost=3,       # iterasi — lebih tinggi = lebih lambat = lebih aman
    memory_cost=65536, # 64MB memory yang dibutuhkan — mencegah GPU attack
    parallelism=2      # thread yang digunakan
)
hashed = ph.hash(password)
is_valid = ph.verify(hashed, password)

# Prinsip 2: Rate limiting dan account lockout
# ANTI-PATTERN: tidak ada batasan percobaan login
@app.route('/login', methods=['POST'])
def login():
    user = find_user(request.json['email'])
    if user and check_password(request.json['password'], user.password_hash):
        return token_response(user)
    return error_response(401)

# BENAR: batasi percobaan, implementasikan lockout
from flask_limiter import Limiter
limiter = Limiter(key_func=get_remote_address)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute; 20 per hour")
def login():
    email = request.json.get('email', '')
    password = request.json.get('password', '')
    user = find_user_by_email(email)

    # Selalu jalankan password check (cegah timing attack)
    if user is None or not verify_password(password, user.password_hash):
        log_failed_attempt(email, request.remote_addr)
        # Pesan yang sama untuk kedua kasus — jangan bocorkan apakah email ada
        return jsonify({'error': 'Invalid email or password'}), 401

    # Regenerasi session setelah login berhasil (cegah session fixation)
    session.regenerate()
    return token_response(user)
Checklist Authentication:

  □ Password di-hash dengan Argon2id, bcrypt, atau scrypt — bukan MD5/SHA256
  □ Rate limiting aktif di endpoint login (5 per menit per IP adalah titik awal)
  □ Pesan error tidak membedakan "email tidak ada" vs "password salah"
  □ Session ID di-regenerasi setelah login berhasil
  □ MFA tersedia untuk semua akun, wajib untuk akun admin
  □ Password reset menggunakan cryptographically random token dengan TTL pendek
  □ "Remember me" menggunakan rotating refresh token, bukan session yang tidak expired

Access Control Cheat Sheet #

Access control yang benar berarti setiap operasi divalidasi di server berdasarkan siapa yang melakukannya, bukan hanya apakah mereka sudah login.

# Prinsip: deny by default, grant explicitly
# ANTI-PATTERN: izin berdasarkan apa yang tidak ada di blacklist
def get_user_data(user_id):
    if current_user.is_banned:  # hanya cek blacklist
        abort(403)
    return User.query.get(user_id)  # semua user bisa akses data siapapun

# BENAR: cek ownership dan permission secara eksplisit
def get_user_data(user_id):
    # Deny by default: hanya boleh akses data sendiri
    if current_user.id != user_id:
        # Kecuali admin — grant explicitly
        if not current_user.has_permission('users:read_any'):
            abort(403)  # Forbidden
    return User.query.get_or_404(user_id)

# Prinsip: gunakan RBAC yang ketat
class Permission:
    USERS_READ_OWN   = 'users:read_own'
    USERS_READ_ANY   = 'users:read_any'
    USERS_WRITE_OWN  = 'users:write_own'
    USERS_WRITE_ANY  = 'users:write_any'
    ORDERS_READ      = 'orders:read'
    ADMIN_ACCESS     = 'admin:access'

ROLE_PERMISSIONS = {
    'user':  [Permission.USERS_READ_OWN, Permission.USERS_WRITE_OWN,
              Permission.ORDERS_READ],
    'staff': [Permission.USERS_READ_ANY, Permission.ORDERS_READ],
    'admin': [Permission.USERS_READ_ANY, Permission.USERS_WRITE_ANY,
              Permission.ORDERS_READ, Permission.ADMIN_ACCESS],
}

def require_permission(permission):
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            user_permissions = ROLE_PERMISSIONS.get(current_user.role, [])
            if permission not in user_permissions:
                abort(403)
            return f(*args, **kwargs)
        return decorated
    return decorator

Input Validation Cheat Sheet #

Semua input dari luar harus dianggap tidak aman sampai terbukti sebaliknya. Validasi di client-side hanyalah untuk UX — tidak bisa diandalkan untuk keamanan.

# Prinsip: whitelist, bukan blacklist
# ANTI-PATTERN: blacklist karakter berbahaya
def validate_username(username):
    forbidden = ['<', '>', '"', "'", ';', '--']
    for char in forbidden:
        if char in username:
            raise ValueError("Invalid character")
    return username
# Masalah: blacklist tidak pernah lengkap — ada encoding trick yang melewati ini

# BENAR: whitelist — hanya izinkan yang eksplisit diperbolehkan
import re

def validate_username(username):
    if not username:
        raise ValueError("Username required")
    if len(username) < 3 or len(username) > 30:
        raise ValueError("Username must be 3-30 characters")
    # Hanya izinkan huruf, angka, underscore, dan dash
    if not re.match(r'^[a-zA-Z0-9_-]+$', username):
        raise ValueError("Username may only contain letters, numbers, _ and -")
    return username

# Prinsip: encode output untuk mencegah XSS
# ANTI-PATTERN: render user input langsung ke HTML
def render_comment(comment_text):
    return f"<div class='comment'>{comment_text}</div>"
# Jika comment_text = "<script>alert('xss')</script>", script dieksekusi

# BENAR: encode sebelum render
import html

def render_comment(comment_text):
    safe_text = html.escape(comment_text)  # < → &lt;, > → &gt;, dsb
    return f"<div class='comment'>{safe_text}</div>"

# Atau di template engine (lebih aman karena auto-escape):
# Jinja2: {{ comment.text }}  ← auto-escaped
# Jinja2: {{ comment.text | safe }}  ← ← HANYA untuk konten yang sudah aman

Session Management Cheat Sheet #

Session yang tidak dikelola dengan benar memungkinkan session fixation, session hijacking, dan masalah lain yang mengakibatkan account takeover.

# Konfigurasi cookie yang aman
app.config.update(
    SESSION_COOKIE_SECURE=True,      # hanya dikirim via HTTPS
    SESSION_COOKIE_HTTPONLY=True,    # tidak bisa diakses via JavaScript
    SESSION_COOKIE_SAMESITE='Lax',  # melindungi dari CSRF
    SESSION_COOKIE_NAME='__Host-session',  # prefix __Host- menambah keamanan
    PERMANENT_SESSION_LIFETIME=timedelta(hours=8),  # absolute timeout
)

# Prinsip: session ID yang kuat
# ANTI-PATTERN: session ID yang mudah ditebak
def generate_session_id():
    return str(user.id) + str(int(time.time()))  # sequential dan predictable

# BENAR: cryptographically random session ID
import secrets
def generate_session_id():
    return secrets.token_urlsafe(32)  # 256-bit entropy

# Prinsip: invalidasi session saat logout
# ANTI-PATTERN: hanya hapus cookie di client
@app.route('/logout')
def logout():
    response = make_response(redirect('/'))
    response.delete_cookie('session')  # cookie dihapus tapi session di server masih ada
    return response

# BENAR: invalidasi session di server DAN hapus cookie
@app.route('/logout')
def logout():
    session_id = request.cookies.get('session')
    if session_id:
        invalidate_server_session(session_id)  # hapus dari Redis/DB
    response = make_response(redirect('/'))
    response.delete_cookie('session')
    return response

Security Headers Cheat Sheet #

HTTP security headers adalah lapisan perlindungan yang bisa ditambahkan tanpa mengubah satu baris logika aplikasi. Setiap header menginstruksikan browser untuk membatasi behavior tertentu yang bisa dieksploitasi.

# Setup security headers di Flask (atau gunakan middleware serupa)
from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)
Talisman(app,
    # Content Security Policy — instruksikan browser hanya jalankan script dari domain sendiri
    content_security_policy={
        'default-src': "'self'",
        'script-src': ["'self'", "https://cdn.trusted.com"],
        'style-src': ["'self'", "'unsafe-inline'"],  # bisa diperktat lebih lanjut
        'img-src': ["'self'", "data:", "https:"],
        'font-src': ["'self'", "https://fonts.googleapis.com"],
        'frame-ancestors': "'none'",  # mencegah embedding di iframe (anti-clickjacking)
    },
    # HTTP Strict Transport Security — paksa HTTPS selama 1 tahun
    strict_transport_security=True,
    strict_transport_security_max_age=31536000,
    strict_transport_security_include_subdomains=True,
    # Referrer Policy
    referrer_policy='strict-origin-when-cross-origin',
    # Mencegah MIME type sniffing
    content_type_options=True,
    # Izinkan frame-ancestors dari CSP di atas yang sudah diatur
    frame_options=None,
)
Penjelasan setiap header dan dampak jika tidak ada:

  Content-Security-Policy (CSP):
  → Mencegah XSS dengan membatasi sumber script yang diizinkan
  → Tanpa CSP: <script src="https://evil.com/steal.js"> bisa diinjeksikan

  Strict-Transport-Security (HSTS):
  → Memaksa browser selalu pakai HTTPS untuk domain ini
  → Tanpa HSTS: attacker di jaringan yang sama bisa strip HTTPS (SSL stripping)

  X-Content-Type-Options: nosniff
  → Mencegah browser "menebak" tipe file dan mengeksekusi HTML sebagai script
  → Tanpa ini: upload file .jpg yang sebenarnya berisi HTML bisa dieksekusi

  X-Frame-Options atau CSP frame-ancestors:
  → Mencegah halaman di-embed dalam iframe (clickjacking)
  → Tanpa ini: penyerang bisa menaruh halaman banking transparan di atas tombol yang menipu

  Referrer-Policy:
  → Mengontrol informasi apa yang dikirim di header Referer
  → Tanpa ini: URL dengan token sensitif (misalnya password reset) bisa bocor ke analitik

CORS Cheat Sheet #

CORS (Cross-Origin Resource Sharing) mengontrol domain mana yang boleh membuat request ke API. Konfigurasi yang terlalu longgar memungkinkan website jahat membuat request atas nama user.

# ANTI-PATTERN: mengizinkan semua origin
from flask_cors import CORS
CORS(app, origins="*")
# Setiap website bisa membuat request ke API ini menggunakan session user
# → data user bisa dicuri oleh situs jahat

# BENAR: whitelist origin secara eksplisit
ALLOWED_ORIGINS = [
    "https://app.yourdomain.com",
    "https://admin.yourdomain.com",
]

CORS(app,
    origins=ALLOWED_ORIGINS,
    methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
    supports_credentials=True,  # hanya jika memang butuh kirim cookies
    max_age=3600  # cache preflight response selama 1 jam
)

# Validasi manual untuk kasus khusus
@app.before_request
def validate_origin():
    origin = request.headers.get('Origin')
    if origin and origin not in ALLOWED_ORIGINS:
        # Log untuk monitoring — bisa jadi serangan atau misconfiguration
        log_suspicious_origin(origin, request.path)
Aturan CORS yang sering salah dipahami:

  ✗ "CORS adalah security feature yang melindungi server"
  ✓ CORS adalah browser policy — ia melindungi USER dari server jahat
    Server bisa selalu menerima request dari manapun.
    CORS hanya mencegah BROWSER mengirim credential ke origin yang tidak diizinkan.

  ✗ "Dengan mengizinkan * saya tidak ada masalah karena saya tidak pakai credentials"
  ✓ Jika API memerlukan header Authorization (JWT), * memang tidak memungkinkan
    credentials. Tapi * masih mengizinkan unauthenticated requests dari mana saja.

  ✗ "Bisa restrict di nginx/proxy saja"
  ✓ CORS harus di-handle di level aplikasi yang tahu tentang authentication context.

File Upload Cheat Sheet #

File upload adalah salah satu attack surface yang paling sering dieksploitasi. Attacker bisa mengunggah file yang terlihat seperti gambar tapi sebenarnya adalah executable yang dijalankan di server.

import magic
import hashlib
import os

ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/gif', 'image/webp'}
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB
UPLOAD_FOLDER = '/var/uploads'  # di luar web root!

def validate_and_save_upload(file):
    # 1. Validasi ukuran
    file.seek(0, 2)
    file_size = file.tell()
    file.seek(0)

    if file_size > MAX_FILE_SIZE:
        raise ValueError(f"File terlalu besar. Maksimum {MAX_FILE_SIZE // 1024 // 1024}MB")

    # 2. Validasi MIME type dari konten file, bukan dari extension atau Content-Type header
    # (keduanya bisa dipalsukan oleh attacker)
    file_content = file.read(2048)  # baca magic bytes
    file.seek(0)

    detected_type = magic.from_buffer(file_content, mime=True)
    if detected_type not in ALLOWED_MIME_TYPES:
        raise ValueError(f"Tipe file tidak diizinkan: {detected_type}")

    # 3. Generate nama file baru — jangan gunakan nama dari user!
    # Nama file dari user bisa mengandung path traversal: ../../etc/passwd
    file_content_full = file.read()
    file_hash = hashlib.sha256(file_content_full).hexdigest()
    extension = {'image/jpeg': '.jpg', 'image/png': '.png',
                 'image/gif': '.gif', 'image/webp': '.webp'}[detected_type]
    safe_filename = f"{file_hash}{extension}"

    # 4. Simpan di luar web root
    save_path = os.path.join(UPLOAD_FOLDER, safe_filename)
    with open(save_path, 'wb') as f:
        f.write(file_content_full)

    return safe_filename
Prinsip-prinsip file upload yang aman:

  ✓ Simpan file di luar web root — file yang diupload tidak boleh bisa
    diakses langsung via URL tanpa melalui aplikasi
  ✓ Validasi MIME type dari konten file, bukan dari extension
    Extension .jpg bisa diubah menjadi .php setelah upload
  ✓ Generate nama file baru yang random/hash — jangan gunakan nama dari user
    Path traversal: ../../etc/passwd sebagai nama file
  ✓ Scan file dengan antivirus sebelum diproses (untuk dokumen, bukan gambar)
  ✓ Batasi ukuran file dan tipe yang diizinkan
  ✗ Jangan execute atau serve file langsung dari direktori upload
  ✗ Jangan simpan metadata upload (nama file asli) tanpa sanitasi

JWT Cheat Sheet #

JWT (JSON Web Token) sering diimplementasikan dengan cara yang membuka celah keamanan serius, terutama karena fleksibilitas format-nya yang bisa disalahgunakan.

import jwt
from datetime import datetime, timedelta

SECRET_KEY = os.environ['JWT_SECRET_KEY']  # minimal 256-bit random

# ANTI-PATTERN: tidak verifikasi algorithm
def verify_token_unsafe(token):
    # Jika attacker mengirim token dengan algorithm "none",
    # beberapa library lama menerimanya tanpa signature verification!
    payload = jwt.decode(token, options={"verify_signature": False})
    return payload

# ANTI-PATTERN: tidak set expiry
def create_token_unsafe(user_id):
    payload = {'user_id': user_id}  # tidak ada exp claim
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')
# Token ini berlaku selamanya — jika dicuri, tidak ada cara untuk invalidasi

# BENAR: konfigurasi yang aman
def create_access_token(user_id):
    now = datetime.utcnow()
    payload = {
        'user_id': user_id,
        'iat': now,                                  # issued at
        'exp': now + timedelta(minutes=15),          # expiry pendek untuk access token
        'jti': secrets.token_urlsafe(16),            # unique token ID untuk revocation
        'type': 'access'                             # bedakan access vs refresh token
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def verify_access_token(token):
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=['HS256'],  # eksplisit tentukan algorithm yang diizinkan
            options={'require': ['exp', 'iat', 'jti', 'type']}
        )
        if payload.get('type') != 'access':
            raise jwt.InvalidTokenError("Wrong token type")
        # Cek apakah token sudah di-revoke (opsional, untuk logout)
        if is_token_revoked(payload['jti']):
            raise jwt.InvalidTokenError("Token has been revoked")
        return payload
    except jwt.ExpiredSignatureError:
        raise AuthError("Token expired")
    except jwt.InvalidTokenError as e:
        raise AuthError(f"Invalid token: {e}")
Kesalahan JWT yang harus dihindari:

  ✗ Menggunakan algorithm "none" — mematikan signature verification
  ✗ Menyimpan data sensitif (password, PII) di payload — payload bisa di-decode siapapun
  ✗ Access token dengan expiry panjang (> 1 jam) — sulit di-revoke jika dicuri
  ✗ Tidak memvalidasi "alg" header — algoritma RS256 dan HS256 punya implikasi berbeda
  ✗ Secret key yang lemah untuk HS256 — mudah di-brute force
  ✓ Gunakan RS256 (asymmetric) jika token dikonsumsi oleh service berbeda
  ✓ Access token pendek (15 menit) + refresh token yang bisa di-revoke

OAuth Security Cheat Sheet #

OAuth 2.0 adalah standar yang kompleks dengan banyak edge case keamanan. Implementasi yang salah bisa membuka celah authorization bypass atau token theft.

# Prinsip: selalu validasi redirect_uri secara eksplisit
# ANTI-PATTERN: menerima redirect_uri apa saja
@app.route('/oauth/authorize')
def oauth_authorize():
    redirect_uri = request.args.get('redirect_uri')
    # Tidak ada validasi — attacker bisa set redirect_uri ke domain mereka
    # dan token akan dikirim ke sana

# BENAR: whitelist redirect_uri yang terdaftar per client
REGISTERED_CLIENTS = {
    'client_id_123': {
        'name': 'Mobile App',
        'allowed_redirect_uris': [
            'https://app.example.com/callback',
            'myapp://oauth/callback',  # deep link untuk mobile
        ]
    }
}

@app.route('/oauth/authorize')
def oauth_authorize():
    client_id = request.args.get('client_id')
    redirect_uri = request.args.get('redirect_uri')
    state = request.args.get('state')  # wajib ada untuk CSRF protection

    client = REGISTERED_CLIENTS.get(client_id)
    if not client:
        abort(400, "Unknown client")

    if redirect_uri not in client['allowed_redirect_uris']:
        abort(400, "Invalid redirect_uri")  # tolak — jangan redirect ke URI tidak dikenal

    if not state:
        abort(400, "State parameter required")  # wajib untuk CSRF protection

    # ... proses authorization
Keamanan OAuth yang wajib:

  ✓ Gunakan PKCE (Proof Key for Code Exchange) untuk public clients
    (mobile app, SPA) — mencegah authorization code interception attack
  ✓ Validasi state parameter untuk mencegah CSRF pada OAuth flow
  ✓ Whitelist redirect_uri secara eksplisit per client
  ✓ Gunakan scope yang paling minimal yang dibutuhkan
  ✓ Authorization code hanya boleh digunakan sekali dan expire dalam hitungan menit
  ✗ Jangan gunakan implicit flow untuk token baru — gunakan auth code + PKCE
  ✗ Jangan simpan access token di localStorage (rentan XSS)

Sensitive Data Exposure Cheat Sheet #

Data sensitif yang tidak dilindungi dengan benar bisa bocor melalui berbagai jalur yang tidak terduga — log, error message, API response, atau bahkan URL.

# ANTI-PATTERN: data sensitif muncul di log
import logging
logger = logging.getLogger(__name__)

def process_payment(card_number, cvv, amount):
    logger.info(f"Processing payment: card={card_number}, cvv={cvv}, amount={amount}")
    # Log ini akan menyimpan nomor kartu kredit lengkap!

# BENAR: mask data sensitif sebelum log
def mask_card_number(card_number):
    return f"****-****-****-{card_number[-4:]}"

def process_payment(card_number, cvv, amount):
    logger.info(f"Processing payment: card={mask_card_number(card_number)}, amount={amount}")
    # CVV tidak pernah di-log sama sekali

# ANTI-PATTERN: data sensitif di API response
def get_user_profile(user_id):
    user = User.query.get(user_id)
    return jsonify(user.__dict__)  # includes password_hash, internal_notes, dll

# BENAR: eksplisit tentukan field apa yang di-expose
def get_user_profile(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify({
        'id': user.id,
        'name': user.name,
        'email': user.email,
        'created_at': user.created_at.isoformat(),
        # password_hash, internal_notes, dsb TIDAK dimasukkan
    })
Data yang tidak boleh pernah muncul di log, URL, atau response yang tidak perlu:

  ✗ Password (plaintext maupun hash)
  ✗ CVV / CVC kartu kredit
  ✗ Nomor kartu kredit lengkap (boleh last 4 digit)
  ✗ Session token atau access token
  ✗ API key
  ✗ Password reset token
  ✗ Data medis (diagnosis, obat-obatan)
  ✗ Nomor KTP / paspor lengkap

  Data yang boleh dengan masking:
  ✓ Nomor kartu kredit: ****-****-****-1234
  ✓ Email: a***@mail.com
  ✓ Nomor telepon: +62-812-****-5678

WebSocket Security Cheat Sheet #

WebSocket membuka channel komunikasi persisten antara client dan server. Tanpa perlindungan yang tepat, channel ini bisa disalahgunakan.

// ANTI-PATTERN: tidak validasi origin WebSocket
// Server side (Node.js):
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
    // Tidak ada validasi siapa yang connect
    ws.on('message', (data) => {
        broadcast(data);  // broadcast langsung tanpa validasi
    });
});

// BENAR: validasi origin dan autentikasi
const wss = new WebSocket.Server({
    port: 8080,
    verifyClient: ({ req }, callback) => {
        const origin = req.headers.origin;
        const allowedOrigins = ['https://app.yourdomain.com'];

        if (!allowedOrigins.includes(origin)) {
            callback(false, 403, 'Forbidden');
            return;
        }

        // Verifikasi token JWT dari query string atau header
        const token = new URL(req.url, 'http://localhost').searchParams.get('token');
        try {
            req.user = verifyJWT(token);
            callback(true);
        } catch {
            callback(false, 401, 'Unauthorized');
        }
    }
});

wss.on('connection', (ws, req) => {
    const user = req.user;

    ws.on('message', (data) => {
        // Validasi dan sanitasi pesan sebelum diproses
        try {
            const message = JSON.parse(data);
            if (!isValidMessage(message)) {
                ws.close(1008, 'Invalid message format');
                return;
            }
            handleMessage(user, message);
        } catch {
            ws.close(1008, 'Invalid JSON');
        }
    });
});
Keamanan WebSocket yang wajib:

  ✓ Gunakan wss:// (WebSocket over TLS) — bukan ws://
  ✓ Validasi Origin header saat handshake
  ✓ Autentikasi koneksi dengan JWT atau session token
  ✓ Validasi dan sanitasi semua pesan yang diterima
  ✓ Implementasikan rate limiting per connection
  ✓ Set timeout untuk koneksi yang tidak aktif
  ✗ Jangan percayai data dari WebSocket tanpa validasi
  ✗ Jangan broadcast pesan dari satu user ke semua user tanpa authorization check

Checklist Security Developer #

AUTHENTICATION:
  □ Password hashing: Argon2id / bcrypt / scrypt
  □ Rate limiting di endpoint login dan sensitive operations
  □ MFA tersedia dan dianjurkan
  □ Session di-regenerasi setelah login

ACCESS CONTROL:
  □ Setiap endpoint memverifikasi authorization, bukan hanya authentication
  □ Resource ownership divalidasi di server
  □ Deny by default diterapkan

INPUT:
  □ Semua input divalidasi dengan whitelist approach
  □ Parameterized query untuk semua database operation
  □ Output di-encode sebelum dirender ke HTML

SESSION & COOKIES:
  □ Cookie menggunakan Secure, HttpOnly, SameSite flags
  □ Session di-invalidasi di server saat logout
  □ Session timeout dikonfigurasi

HEADERS:
  □ Content-Security-Policy (CSP)
  □ Strict-Transport-Security (HSTS)
  □ X-Content-Type-Options: nosniff
  □ Referrer-Policy

API:
  □ CORS hanya mengizinkan origin yang terdaftar
  □ Rate limiting aktif di semua endpoint publik
  □ JWT menggunakan algorithm eksplisit dan expiry yang tepat

FILE UPLOAD:
  □ MIME type divalidasi dari konten file, bukan extension
  □ File disimpan di luar web root
  □ Nama file di-generate ulang, tidak menggunakan nama dari user

DATA:
  □ Data sensitif tidak muncul di log
  □ API response hanya expose field yang diperlukan
  □ Encryption at rest untuk data sensitif

Ringkasan #

  • Authentication — Argon2id/bcrypt untuk password, rate limiting pada login, MFA untuk akun sensitif, regenerasi session setelah login, token reset password yang cryptographically random.
  • Access Control — deny by default, verifikasi ownership di setiap request, jangan hanya cek apakah user sudah login.
  • Input Validation — whitelist bukan blacklist, validasi tipe/format/panjang di server, encode output sebelum render (bukan sanitize input).
  • Session Management — cookie dengan Secure + HttpOnly + SameSite, invalidasi session di server saat logout, session timeout.
  • Security Headers — CSP mencegah XSS, HSTS memaksa HTTPS, X-Content-Type-Options mencegah MIME sniffing, X-Frame-Options mencegah clickjacking.
  • CORS — whitelist origin secara eksplisit, jangan gunakan * terutama jika API menggunakan credentials.
  • File Upload — validasi MIME dari konten file, simpan di luar web root, generate nama file baru, jangan serve langsung dari direktori upload.
  • JWT — tentukan algorithm secara eksplisit, set expiry pendek, jangan simpan data sensitif di payload, gunakan PKCE untuk public clients.
  • Sensitive Data — jangan pernah log credential atau token, mask data sensitif di log dan response, enkripsi at rest.
  • WebSocket — gunakan wss://, validasi origin saat handshake, autentikasi setiap koneksi, validasi semua pesan yang diterima.

← Sebelumnya: OWASP   Berikutnya: SQL Injection →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact