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) // < → <, > → >, 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.
#
- 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.