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) // < → &lt;, > → &gt;, 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.

← Sebelumnya: OWASP   Berikutnya: SQL Injection →

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