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.
// ANTI-PATTERN: MD5 atau SHA256 tanpa salt — cepat, mudah di-crack
hashed := md5.Sum([]byte(password)) // JANGAN PERNAH LAKUKAN INI
// BENAR: Argon2id (rekomendasi terkuat saat ini)
// golang.org/x/crypto/argon2
func hashPassword(password string, salt []byte) string {
// time = 3 iterasi — lebih tinggi = lebih lambat = lebih aman
// memory = 64MB — mencegah GPU attack
// threads = 2
hash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 2, 32)
return fmt.Sprintf("$argon2id$v=19$m=65536,t=3,p=2$%s$%s",
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash))
}
func verifyPassword(stored string, password string, salt []byte) bool {
expected := argon2.IDKey([]byte(password), salt, 3, 64*1024, 2, 32)
return subtle.ConstantTimeCompare([]byte(stored), expected) == 1
}
// ANTI-PATTERN: tidak ada batasan percobaan login
func loginHandler(w http.ResponseWriter, r *http.Request) {
user := findUser(r.FormValue("email"))
if user != nil && checkPassword(r.FormValue("password"), user.PasswordHash) {
issueToken(w, user)
return
}
writeError(w, 401)
}
// BENAR: batasi percobaan, implementasikan lockout
func loginHandler(w http.ResponseWriter, r *http.Request) {
if !allowAttempt(r.RemoteAddr) { // penghitung sliding-window per IP
writeError(w, 429) // Too Many Requests
return
}
email := r.FormValue("email")
password := r.FormValue("password")
user := findUserByEmail(email)
// Selalu jalankan password check (cegah timing attack)
if user == nil || !verifyPassword(user.PasswordHash, password, user.Salt) {
logFailedAttempt(email, r.RemoteAddr)
// Pesan yang sama untuk kedua kasus — jangan bocorkan apakah email ada
writeJSON(w, 401, map[string]string{"error": "Invalid email or password"})
return
}
// Regenerasi session setelah login berhasil (cegah session fixation)
regenerateSession(w, r)
issueToken(w, 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.
// ANTI-PATTERN: izin berdasarkan apa yang tidak ada di blacklist
func getUserData(userID string) *User {
if currentUser.IsBanned { // hanya cek blacklist
return nil // 403
}
return findUserByID(userID) // semua user bisa akses data siapapun
}
// BENAR: cek ownership dan permission secara eksplisit
func getUserData(w http.ResponseWriter, r *http.Request, userID string) {
// Deny by default: hanya boleh akses data sendiri
if currentUser.ID != userID {
// Kecuali admin — grant explicitly
if !currentUser.HasPermission("users:read_any") {
http.Error(w, "Forbidden", 403)
return
}
}
user := findUserOr404(userID)
// ... use user
}
// BENAR: gunakan RBAC yang ketat
type Permission string
const (
UsersReadOwn Permission = "users:read_own"
UsersReadAny Permission = "users:read_any"
UsersWriteOwn Permission = "users:write_own"
UsersWriteAny Permission = "users:write_any"
OrdersRead Permission = "orders:read"
AdminAccess Permission = "admin:access"
)
var rolePermissions = map[string][]Permission{
"user": {UsersReadOwn, UsersWriteOwn, OrdersRead},
"staff": {UsersReadAny, OrdersRead},
"admin": {UsersReadAny, UsersWriteAny, OrdersRead, AdminAccess},
}
// Middleware yang memberlakukan sebuah permission
func requirePermission(permission Permission, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
userPermissions := rolePermissions[currentUser.Role]
if !containsPermission(userPermissions, permission) {
http.Error(w, "Forbidden", 403)
return
}
next(w, r)
}
}
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.
// ANTI-PATTERN: blacklist karakter berbahaya
func validateUsername(username string) (string, error) {
forbidden := []string{"<", ">", "\"", "'", ";", "--"}
for _, ch := range forbidden {
if strings.Contains(username, ch) {
return "", errors.New("invalid character")
}
}
return username, nil
}
// Masalah: blacklist tidak pernah lengkap — ada encoding trick yang melewati ini
// BENAR: whitelist — hanya izinkan yang eksplisit diperbolehkan
func validateUsername(username string) (string, error) {
if username == "" {
return "", errors.New("username required")
}
if len(username) < 3 || len(username) > 30 {
return "", errors.New("username must be 3-30 characters")
}
// Hanya izinkan huruf, angka, underscore, dan dash
if !usernameRegex.MatchString(username) {
return "", errors.New("username may only contain letters, numbers, _ and -")
}
return username, nil
}
// ANTI-PATTERN: render user input langsung ke HTML
func renderComment(commentText string) string {
return fmt.Sprintf("<div class='comment'>%s</div>", commentText)
}
// If commentText = "<script>alert('xss')</script>", script dieksekusi
// BENAR: encode sebelum render (html/template auto-escape)
func renderComment(commentText string) string {
safeText := html.EscapeString(commentText) // < → <, > → >, etc.
return fmt.Sprintf("<div class='comment'>%s</div>", safeText)
}
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
http.SetCookie(w, &http.Cookie{
Name: "__Host-session", // the __Host- prefix adds security
Value: sessionID,
Secure: true, // hanya dikirim via HTTPS
HttpOnly: true, // tidak bisa diakses via JavaScript
SameSite: http.SameSiteLaxMode, // melindungi dari CSRF
Path: "/",
MaxAge: 8 * 3600, // absolute timeout: 8 jam
})
// ANTI-PATTERN: session ID yang mudah ditebak
func generateSessionID() string {
return strconv.Itoa(user.ID) + strconv.FormatInt(time.Now().Unix(), 10) // sequential dan predictable
}
// BENAR: cryptographically random session ID
func generateSessionID() string {
b := make([]byte, 32)
rand.Read(b) // crypto/rand — 256-bit entropy
return base64.RawURLEncoding.EncodeToString(b)
}
// ANTI-PATTERN: hanya hapus cookie di client
func logoutHandler(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1})
// cookie dihapus tapi session di server masih ada
}
// BENAR: invalidasi session di server DAN hapus cookie
func logoutHandler(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("session"); err == nil {
invalidateServerSession(c.Value) // hapus dari Redis/DB
}
http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1})
}
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 (middleware net/http)
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Content Security Policy — instruksikan browser hanya jalankan script dari domain sendiri
w.Header().Set("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self' https://cdn.trusted.com; "+
"style-src 'self' 'unsafe-inline'; "+
"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
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// Referrer Policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
// Mencegah MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
next.ServeHTTP(w, r)
})
}
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
w.Header().Set("Access-Control-Allow-Origin", "*")
// Setiap website bisa membuat request ke API ini menggunakan session user
// → data user bisa dicuri oleh situs jahat
// BENAR: whitelist origin secara eksplisit
var allowedOrigins = map[string]bool{
"https://app.yourdomain.com": true,
"https://admin.yourdomain.com": true,
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true") // hanya jika memang butuh kirim cookies
w.Header().Set("Access-Control-Max-Age", "3600") // cache preflight response selama 1 jam
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
// Validasi manual untuk kasus khusus
if origin != "" && !allowedOrigins[origin] {
// Log untuk monitoring — bisa jadi serangan atau misconfiguration
logSuspiciousOrigin(origin, r.URL.Path)
}
next.ServeHTTP(w, r)
})
}
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 (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
)
var allowedMimeTypes = map[string]string{
"image/jpeg": ".jpg",
"image/png": ".png",
"image/gif": ".gif",
"image/webp": ".webp",
}
const maxFileSize = 5 * 1024 * 1024 // 5MB
const uploadFolder = "/var/uploads" // di luar web root!
func validateAndSaveUpload(r *http.Request) (string, error) {
file, header, err := r.FormFile("file")
if err != nil {
return "", err
}
defer file.Close()
// 1. Validasi ukuran
if header.Size > maxFileSize {
return "", fmt.Errorf("file too large. maximum %dMB", maxFileSize/1024/1024)
}
// 2. Validasi MIME type dari konten file, bukan dari extension atau Content-Type header
// (keduanya bisa dipalsukan oleh attacker)
buf := make([]byte, 2048) // baca magic bytes
n, _ := file.Read(buf)
detectedType := http.DetectContentType(buf[:n])
ext, ok := allowedMimeTypes[detectedType]
if !ok {
return "", fmt.Errorf("file type not allowed: %s", detectedType)
}
// 3. Generate nama file baru — jangan gunakan nama dari user!
// Nama file dari user bisa mengandung path traversal: ../../etc/passwd
full, _ := io.ReadAll(file)
sum := sha256.Sum256(full)
safeFilename := hex.EncodeToString(sum[:]) + ext
// 4. Simpan di luar web root
savePath := filepath.Join(uploadFolder, safeFilename)
if err := os.WriteFile(savePath, full, 0o600); err != nil {
return "", err
}
return safeFilename, nil
}
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.
// JWT handling — github.com/golang-jwt/jwt/v5
var secretKey = []byte(os.Getenv("JWT_SECRET_KEY")) // minimal 256-bit random
// ANTI-PATTERN: tidak verifikasi algorithm
func verifyTokenUnsafe(token string) (jwt.MapClaims, error) {
// Jika attacker mengirim token dengan algorithm "none",
// beberapa library lama menerimanya tanpa signature verification!
return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return secretKey, nil // no algorithm check
})
}
// ANTI-PATTERN: tidak set expiry
func createTokenUnsafe(userID string) (string, error) {
claims := jwt.MapClaims{"user_id": userID} // tidak ada exp claim
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretKey)
}
// Token ini berlaku selamanya — jika dicuri, tidak ada cara untuk invalidasi
// BENAR: konfigurasi yang aman
func createAccessToken(userID string) (string, error) {
now := time.Now()
claims := jwt.MapClaims{
"user_id": userID,
"iat": now.Unix(), // issued at
"exp": now.Add(15 * time.Minute).Unix(), // expiry pendek untuk access token
"jti": randomToken(16), // unique token ID untuk revocation
"type": "access", // bedakan access vs refresh token
}
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretKey)
}
func verifyAccessToken(token string) (jwt.MapClaims, error) {
claims, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
// eksplisit tentukan algorithm yang diizinkan
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return secretKey, nil
})
if err != nil {
return nil, err
}
mapClaims := claims.Claims.(jwt.MapClaims)
if mapClaims["type"] != "access" {
return nil, errors.New("wrong token type")
}
// Cek apakah token sudah di-revoke (opsional, untuk logout)
if isTokenRevoked(mapClaims["jti"].(string)) {
return nil, errors.New("token has been revoked")
}
return mapClaims, nil
}
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.
// ANTI-PATTERN: menerima redirect_uri apa saja
func oauthAuthorizeHandler(w http.ResponseWriter, r *http.Request) {
redirectURI := r.URL.Query().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
type OAuthClient struct {
Name string
AllowedRedirectURIs []string
}
var registeredClients = map[string]OAuthClient{
"client_id_123": {
Name: "Mobile App",
AllowedRedirectURIs: []string{
"https://app.example.com/callback",
"myapp://oauth/callback", // deep link untuk mobile
},
},
}
func oauthAuthorizeHandler(w http.ResponseWriter, r *http.Request) {
clientID := r.URL.Query().Get("client_id")
redirectURI := r.URL.Query().Get("redirect_uri")
state := r.URL.Query().Get("state") // wajib untuk CSRF protection
client, ok := registeredClients[clientID]
if !ok {
http.Error(w, "Unknown client", http.StatusBadRequest)
return
}
if !slices.Contains(client.AllowedRedirectURIs, redirectURI) {
http.Error(w, "Invalid redirect_uri", http.StatusBadRequest) // tolak — jangan redirect ke URI tidak dikenal
return
}
if state == "" {
http.Error(w, "State parameter required", http.StatusBadRequest) // wajib untuk CSRF protection
return
}
// ... 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
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
func processPayment(cardNumber, cvv string, amount int) {
logger.Info("Processing payment",
"card", cardNumber, "cvv", cvv, "amount", amount)
// Log ini akan menyimpan nomor kartu kredit lengkap!
}
// BENAR: mask data sensitif sebelum log
func maskCardNumber(cardNumber string) string {
if len(cardNumber) < 4 {
return "****"
}
return "****-****-****-" + cardNumber[len(cardNumber)-4:]
}
func processPayment(cardNumber, cvv string, amount int) {
logger.Info("Processing payment",
"card", maskCardNumber(cardNumber), "amount", amount)
// CVV tidak pernah di-log sama sekali
}
// ANTI-PATTERN: data sensitif di API response
func getUserProfile(userID string) map[string]any {
user := findUser(userID)
// termasuk password_hash, internal_notes, dll
return map[string]any{
"id": user.ID, "name": user.Name, "email": user.Email,
"password_hash": user.PasswordHash, "internal_notes": user.InternalNotes,
}
}
// BENAR: eksplisit tentukan field apa yang di-expose
func getUserProfile(userID string) map[string]any {
user := findUserOr404(userID)
return map[string]any{
"id": user.ID,
"name": user.Name,
"email": user.Email,
"created_at": user.CreatedAt,
// 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 (gorilla/websocket)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Tidak ada validasi siapa yang connect
},
}
// BENAR: validasi origin dan autentikasi
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
allowedOrigins := []string{"https://app.yourdomain.com"}
if !slices.Contains(allowedOrigins, origin) {
return false // 403 Forbidden
}
// Verifikasi token JWT dari query string atau header
token := r.URL.Query().Get("token")
user, err := verifyJWT(token)
if err != nil {
return false // 401 Unauthorized
}
r = r.WithContext(context.WithValue(r.Context(), "user", user))
return true
},
}
func handleConnection(w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
defer conn.Close()
user := r.Context().Value("user")
for {
_, data, err := conn.ReadMessage()
if err != nil {
break
}
// Validasi dan sanitasi pesan sebelum diproses
var message Message
if err := json.Unmarshal(data, &message); err != nil {
conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(1008, "Invalid JSON"))
continue
}
if !isValidMessage(message) {
conn.WriteMessage(websocket.CloseMessage,
websocket.FormatCloseMessage(1008, "Invalid message format"))
continue
}
handleMessage(user, message)
}
}
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.