OWASP Cheatsheet #

OWASP Cheat Sheets is a collection of practical guides curated from the experience of thousands of security engineers worldwide. Unlike the OWASP Top 10, which describes risks, Cheat Sheets provide implementation guidance — what to do and how to do it correctly.

This article summarizes the cheat sheets most relevant to backend and fullstack developers, complete with concrete code examples and anti-patterns to avoid. It’s designed as a reference to consult when building new features, doing security reviews, or when unsure about the correct way to implement something.

Authentication Cheat Sheet #

Weak authentication is one of the most common causes of account takeover. Most attacks succeed not because attackers have extraordinary abilities, but because systems lack basic protections.

// ANTI-PATTERN: MD5 or SHA256 without salts — fast, easy to crack
hashed := md5.Sum([]byte(password)) // NEVER DO THIS

// CORRECT: Argon2id (the current strongest recommendation)
// golang.org/x/crypto/argon2
func hashPassword(password string, salt []byte) string {
    // time = 3 iterations — higher = slower = more secure
    // memory = 64MB — prevents GPU attacks
    // 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: no login attempt limits
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)
}

// CORRECT: limit attempts, implement lockout
func loginHandler(w http.ResponseWriter, r *http.Request) {
    if !allowAttempt(r.RemoteAddr) { // sliding-window counter per IP
        writeError(w, 429) // Too Many Requests
        return
    }
    email := r.FormValue("email")
    password := r.FormValue("password")
    user := findUserByEmail(email)

    // Always run the password check (prevents timing attacks)
    if user == nil || !verifyPassword(user.PasswordHash, password, user.Salt) {
        logFailedAttempt(email, r.RemoteAddr)
        // The same message for both cases — don't leak whether the email exists
        writeJSON(w, 401, map[string]string{"error": "Invalid email or password"})
        return
    }

    // Regenerate the session after successful login (prevents session fixation)
    regenerateSession(w, r)
    issueToken(w, user)
}
Authentication Checklist:

  □ Passwords hashed with Argon2id, bcrypt, or scrypt — not MD5/SHA256
  □ Rate limiting active on login endpoints (5 per minute per IP is a starting point)
  □ Error messages don't distinguish "email not found" vs "wrong password"
  □ Session IDs regenerated after successful login
  □ MFA available for all accounts, mandatory for admin accounts
  □ Password resets use cryptographically random tokens with short TTLs
  □ "Remember me" uses rotating refresh tokens, not non-expiring sessions

Access Control Cheat Sheet #

Correct access control means every operation is validated server-side based on who performs it, not just whether they’re logged in.

// ANTI-PATTERN: permissions based on what's not on a blacklist
func getUserData(userID string) *User {
    if currentUser.IsBanned { // only checks a blacklist
        return nil // 403
    }
    return findUserByID(userID) // any user can access anyone's data
}

// CORRECT: check ownership and permission explicitly
func getUserData(w http.ResponseWriter, r *http.Request, userID string) {
    // Deny by default: only access to your own data
    if currentUser.ID != userID {
        // Unless admin — grant explicitly
        if !currentUser.HasPermission("users:read_any") {
            http.Error(w, "Forbidden", 403)
            return
        }
    }
    user := findUserOr404(userID)
    // ... use user
}

// CORRECT: use strict RBAC
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 that enforces a 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 #

All external input must be considered unsafe until proven otherwise. Client-side validation is only for UX — it can’t be trusted for security.

// ANTI-PATTERN: blacklisting dangerous characters
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
}
// Problem: blacklists are never complete — encoding tricks get through

// CORRECT: whitelists — only allow what's explicitly permitted
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")
    }
    // Only allow letters, numbers, underscores, and dashes
    if !usernameRegex.MatchString(username) {
        return "", errors.New("username may only contain letters, numbers, _ and -")
    }
    return username, nil
}

// ANTI-PATTERN: rendering user input directly into HTML
func renderComment(commentText string) string {
    return fmt.Sprintf("<div class='comment'>%s</div>", commentText)
}
// If commentText = "<script>alert('xss')</script>", the script executes

// CORRECT: encode before rendering (html/template auto-escapes)
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 #

Sessions not managed correctly enable session fixation, session hijacking, and other problems leading to account takeover.

// Secure cookie configuration
http.SetCookie(w, &http.Cookie{
    Name:     "__Host-session", // the __Host- prefix adds security
    Value:    sessionID,
    Secure:   true,   // only sent over HTTPS
    HttpOnly: true,   // inaccessible via JavaScript
    SameSite: http.SameSiteLaxMode, // protects against CSRF
    Path:     "/",
    MaxAge:   8 * 3600, // absolute timeout: 8 hours
})

// ANTI-PATTERN: guessable session IDs
func generateSessionID() string {
    return strconv.Itoa(user.ID) + strconv.FormatInt(time.Now().Unix(), 10) // sequential and predictable
}

// CORRECT: cryptographically random session IDs
func generateSessionID() string {
    b := make([]byte, 32)
    rand.Read(b) // crypto/rand — 256-bit entropy
    return base64.RawURLEncoding.EncodeToString(b)
}

// ANTI-PATTERN: only deleting the client cookie
func logoutHandler(w http.ResponseWriter, r *http.Request) {
    http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1})
    // cookie deleted but the server session still exists
}

// CORRECT: invalidate the server session AND delete the cookie
func logoutHandler(w http.ResponseWriter, r *http.Request) {
    if c, err := r.Cookie("session"); err == nil {
        invalidateServerSession(c.Value) // remove from Redis/DB
    }
    http.SetCookie(w, &http.Cookie{Name: "session", MaxAge: -1})
}

Security Headers Cheat Sheet #

HTTP security headers are a protection layer that can be added without changing a single line of application logic. Each header instructs the browser to restrict certain exploitable behaviors.

// Setting up security headers (net/http middleware)
func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Content Security Policy — instruct the browser to only run scripts from your domain
        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'") // prevents iframe embedding (anti-clickjacking)
        // HTTP Strict Transport Security — force HTTPS for 1 year
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        // Referrer Policy
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        // Prevents MIME type sniffing
        w.Header().Set("X-Content-Type-Options", "nosniff")
        next.ServeHTTP(w, r)
    })
}
Explanation of each header and the impact if missing:

  Content-Security-Policy (CSP):
  → Prevents XSS by restricting allowed script sources
  → Without CSP: <script src="https://evil.com/steal.js"> can be injected

  Strict-Transport-Security (HSTS):
  → Forces browsers to always use HTTPS for this domain
  → Without HSTS: attackers on the same network can strip HTTPS (SSL stripping)

  X-Content-Type-Options: nosniff
  → Prevents browsers from "guessing" file types and executing HTML as scripts
  → Without it: uploading a .jpg that actually contains HTML can be executed

  X-Frame-Options or CSP frame-ancestors:
  → Prevents pages from being embedded in iframes (clickjacking)
  → Without it: attackers can place a transparent banking page over deceptive buttons

  Referrer-Policy:
  → Controls what information is sent in the Referer header
  → Without it: URLs with sensitive tokens (e.g. password resets) can leak to analytics

CORS Cheat Sheet #

CORS (Cross-Origin Resource Sharing) controls which domains may make requests to your API. Overly loose configuration lets malicious websites make requests on behalf of users.

// ANTI-PATTERN: allowing all origins
w.Header().Set("Access-Control-Allow-Origin", "*")
// Any website can make requests to this API using user sessions
// → user data can be stolen by malicious sites

// CORRECT: explicitly whitelist origins
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") // only if credentials really need to be sent
            w.Header().Set("Access-Control-Max-Age", "3600")           // cache the preflight response for 1 hour
        }
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        // Manual validation for special cases
        if origin != "" && !allowedOrigins[origin] {
            // Log for monitoring — could be an attack or misconfiguration
            logSuspiciousOrigin(origin, r.URL.Path)
        }
        next.ServeHTTP(w, r)
    })
}
CORS rules often misunderstood:

  ✗ "CORS is a security feature protecting the server"
  ✓ CORS is a browser policy — it protects USERS from malicious servers
    Servers can always accept requests from anywhere.
    CORS only prevents BROWSERS from sending credentials to disallowed origins.

  ✗ "Allowing * is fine because I don't use credentials"
  ✓ If the API requires an Authorization header (JWT), * indeed doesn't allow
    credentials. But * still allows unauthenticated requests from anywhere.

  ✗ "You can restrict it at the nginx/proxy level"
  ✓ CORS must be handled at the application level, which knows the authentication context.

File Upload Cheat Sheet #

File uploads are one of the most frequently exploited attack surfaces. Attackers can upload files that look like images but are actually executables run on the 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" // outside the web root!

func validateAndSaveUpload(r *http.Request) (string, error) {
    file, header, err := r.FormFile("file")
    if err != nil {
        return "", err
    }
    defer file.Close()

    // 1. Validate the size
    if header.Size > maxFileSize {
        return "", fmt.Errorf("file too large. maximum %dMB", maxFileSize/1024/1024)
    }

    // 2. Validate the MIME type from file content, not the extension or Content-Type header
    // (both can be forged by attackers)
    buf := make([]byte, 2048) // read 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 a new filename — never use the user's filename!
    // User filenames can contain path traversal: ../../etc/passwd
    full, _ := io.ReadAll(file)
    sum := sha256.Sum256(full)
    safeFilename := hex.EncodeToString(sum[:]) + ext

    // 4. Save outside the web root
    savePath := filepath.Join(uploadFolder, safeFilename)
    if err := os.WriteFile(savePath, full, 0o600); err != nil {
        return "", err
    }

    return safeFilename, nil
}
Secure file upload principles:

  ✓ Store files outside the web root — uploaded files must not be
    directly accessible via URLs without going through the application
  ✓ Validate MIME types from file content, not extensions
    A .jpg extension can be renamed to .php after upload
  ✓ Generate new random/hash filenames — never use user-supplied names
    Path traversal: ../../etc/passwd as a filename
  ✓ Scan files with antivirus before processing (for documents, not images)
  ✓ Limit file sizes and allowed types
  ✗ Don't execute or serve files directly from upload directories
  ✗ Don't store upload metadata (original filenames) without sanitization

JWT Cheat Sheet #

JWT (JSON Web Token) is often implemented in ways that open serious security holes, especially because of the format’s flexibility being exploitable.

// JWT handling — github.com/golang-jwt/jwt/v5
var secretKey = []byte(os.Getenv("JWT_SECRET_KEY")) // at least 256-bit random

// ANTI-PATTERN: not verifying the algorithm
func verifyTokenUnsafe(token string) (jwt.MapClaims, error) {
    // If an attacker sends a token with the "none" algorithm,
    // some old libraries accept it without signature verification!
    return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
        return secretKey, nil // no algorithm check
    })
}

// ANTI-PATTERN: no expiry set
func createTokenUnsafe(userID string) (string, error) {
    claims := jwt.MapClaims{"user_id": userID} // no exp claim
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secretKey)
}
// This token is valid forever — if stolen, there's no way to invalidate it

// CORRECT: secure configuration
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(), // short expiry for access tokens
        "jti":     randomToken(16),                  // unique token ID for revocation
        "type":    "access",                         // distinguish access vs refresh tokens
    }
    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) {
        // explicitly specify allowed algorithms
        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")
    }
    // Check whether the token has been revoked (optional, for logout)
    if isTokenRevoked(mapClaims["jti"].(string)) {
        return nil, errors.New("token has been revoked")
    }
    return mapClaims, nil
}
JWT mistakes to avoid:

  ✗ Using the "none" algorithm — disables signature verification
  ✗ Storing sensitive data (passwords, PII) in payloads — anyone can decode payloads
  ✗ Access tokens with long expiries (> 1 hour) — hard to revoke if stolen
  ✗ Not validating the "alg" header — RS256 and HS256 have different implications
  ✗ Weak secret keys for HS256 — easy to brute force
  ✓ Use RS256 (asymmetric) when tokens are consumed by different services
  ✓ Short access tokens (15 minutes) + revocable refresh tokens

OAuth Security Cheat Sheet #

OAuth 2.0 is a complex standard with many security edge cases. Wrong implementations can open authorization bypass or token theft holes.

// ANTI-PATTERN: accepting any redirect_uri
func oauthAuthorizeHandler(w http.ResponseWriter, r *http.Request) {
    redirectURI := r.URL.Query().Get("redirect_uri")
    // No validation — attackers can set redirect_uri to their domain
    // and the token will be sent there
}

// CORRECT: whitelist registered redirect_uris 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 for 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") // required for 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) // reject — don't redirect to unknown URIs
        return
    }

    if state == "" {
        http.Error(w, "State parameter required", http.StatusBadRequest) // required for CSRF protection
        return
    }

    // ... process authorization
}
Mandatory OAuth security:

  ✓ Use PKCE (Proof Key for Code Exchange) for public clients
    (mobile apps, SPAs) — prevents authorization code interception attacks
  ✓ Validate the state parameter to prevent CSRF in OAuth flows
  ✓ Whitelist redirect_uri explicitly per client
  ✓ Use the minimal scopes needed
  ✓ Authorization codes may only be used once and expire within minutes
  ✗ Don't use the implicit flow for new tokens — use auth code + PKCE
  ✗ Don't store access tokens in localStorage (XSS-prone)

Sensitive Data Exposure Cheat Sheet #

Sensitive data not properly protected can leak through various unexpected channels — logs, error messages, API responses, or even URLs.

// ANTI-PATTERN: sensitive data appearing in logs
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)
    // This log stores full credit card numbers!
}

// CORRECT: mask sensitive data before logging
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 is never logged at all
}

// ANTI-PATTERN: sensitive data in API responses
func getUserProfile(userID string) map[string]any {
    user := findUser(userID)
    // includes password_hash, internal_notes, etc.
    return map[string]any{
        "id": user.ID, "name": user.Name, "email": user.Email,
        "password_hash": user.PasswordHash, "internal_notes": user.InternalNotes,
    }
}

// CORRECT: explicitly specify which fields are exposed
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, etc. NOT included
    }
}
Data that must never appear in logs, URLs, or unnecessary responses:

  ✗ Passwords (plaintext or hashes)
  ✗ Credit card CVV/CVC codes
  ✗ Full credit card numbers (last 4 digits are okay)
  ✗ Session tokens or access tokens
  ✗ API keys
  ✗ Password reset tokens
  ✗ Medical data (diagnoses, medications)
  ✗ Full ID card / passport numbers

  Data allowed with masking:
  ✓ Credit card numbers: ****-****-****-1234
  ✓ Emails: a***@mail.com
  ✓ Phone numbers: +62-812-****-5678

WebSocket Security Cheat Sheet #

WebSockets open persistent communication channels between clients and servers. Without proper protection, these channels can be abused.

// ANTI-PATTERN: not validating WebSocket origins (gorilla/websocket)
var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // No validation of who connects
    },
}

// CORRECT: validate origins and authenticate
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
        }

        // Verify the JWT token from the query string or 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
        }
        // Validate and sanitize messages before processing
        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)
    }
}
Mandatory WebSocket security:

  ✓ Use wss:// (WebSocket over TLS) — not ws://
  ✓ Validate the Origin header during the handshake
  ✓ Authenticate connections with JWTs or session tokens
  ✓ Validate and sanitize all received messages
  ✓ Implement per-connection rate limiting
  ✓ Set timeouts for inactive connections
  ✗ Don't trust WebSocket data without validation
  ✗ Don't broadcast one user's messages to all users without authorization checks

Developer Security Checklist #

AUTHENTICATION:
  □ Password hashing: Argon2id / bcrypt / scrypt
  □ Rate limiting on login and sensitive operation endpoints
  □ MFA available and encouraged
  □ Sessions regenerated after login

ACCESS CONTROL:
  □ Every endpoint verifies authorization, not just authentication
  □ Resource ownership validated server-side
  □ Deny by default applied

INPUT:
  □ All input validated with the whitelist approach
  □ Parameterized queries for all database operations
  □ Output encoded before rendering into HTML

SESSION & COOKIES:
  □ Cookies use Secure, HttpOnly, SameSite flags
  □ Sessions invalidated server-side on logout
  □ Session timeouts configured

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

API:
  □ CORS only allows registered origins
  □ Rate limiting active on all public endpoints
  □ JWTs use explicit algorithms and proper expiries

FILE UPLOADS:
  □ MIME types validated from file content, not extensions
  □ Files stored outside the web root
  □ Filenames regenerated, never using user-supplied names

DATA:
  □ Sensitive data doesn't appear in logs
  □ API responses only expose necessary fields
  □ Encryption at rest for sensitive data

Summary #

  • Authentication — Argon2id/bcrypt for passwords, rate limiting on logins, MFA for sensitive accounts, session regeneration after login, cryptographically random password reset tokens.
  • Access Control — deny by default, verify ownership on every request, don’t just check whether users are logged in.
  • Input Validation — whitelists not blacklists, validate types/formats/lengths server-side, encode output before rendering (rather than sanitizing input).
  • Session Management — cookies with Secure + HttpOnly + SameSite, server-side session invalidation on logout, session timeouts.
  • Security Headers — CSP prevents XSS, HSTS forces HTTPS, X-Content-Type-Options prevents MIME sniffing, X-Frame-Options prevents clickjacking.
  • CORS — explicitly whitelist origins, don’t use * especially when APIs use credentials.
  • File Uploads — validate MIME from file content, store outside the web root, generate new filenames, don’t serve directly from upload directories.
  • JWT — specify algorithms explicitly, set short expiries, don’t store sensitive data in payloads, use PKCE for public clients.
  • Sensitive Data — never log credentials or tokens, mask sensitive data in logs and responses, encrypt at rest.
  • WebSocket — use wss://, validate origins at the handshake, authenticate every connection, validate all received messages.
#

← Previous: OWASP   Next: SQL Injection

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