Web Security Cheatsheet

Secrets and Cryptography

Use this Web Security reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Where secrets belong

A secret is anything whose disclosure grants access: API keys, database passwords, signing keys, OAuth client secrets, and private keys. The failure mode is almost never a broken cipher, it is a secret in the wrong place. Cryptographic Failures is A04:2025.

LocationVerdict
Committed to GitNever. History is forever, and a private repo becomes public or gets forked.
Hardcoded in sourceNever. It ships in every artifact and every backup.
In a client bundle or mobile appNever. Anything shipped to a device is public, minification is not protection.
In a container image layerNo. Layers are extractable, including deleted files.
In a CI log or an error messageNo. Redact before logging, and treat a logged secret as compromised.
In a URL query stringNo. It lands in proxy logs, Referer, and browser history.
An env var from a secret manager at startAcceptable, and the common baseline
A secret manager fetched at runtimeBetter, supports rotation without a redeploy
KMS or HSM, key never leavesBest for signing and encryption keys
# .gitignore is a reminder, not a control
.env
.env.*
!.env.example
*.pem
*.key

Ship a committed .env.example with placeholder values so the required names are documented without the values.

Preventing and handling leaks

Detection has to be automatic, because humans miss it.

# Scan history, not just the working tree
gitleaks detect --source . --redact
trufflehog git file://. --only-verified
# Block commits before they happen
pre-commit install     # with a gitleaks or detect-secrets hook configured
PracticeDetail
Pre-commit scanningCheapest place to catch it
CI scanning on every PRCatches what a developer bypassed locally
Provider push protectionGitHub secret scanning blocks known key formats at push
Short-lived credentialsOIDC federation from CI to cloud beats a long-lived key entirely
Least privilege per credentialOne scope, one purpose, so a leak has a bounded blast radius
Scheduled rotationAnd a tested runbook, not a wiki page nobody has run

A leaked secret is compromised, permanently. Rotate it. Do not "remove it in the next commit" and move on. Rewriting history does not help, because the object may already be cloned, cached by the host, or indexed. The order is: rotate, revoke the old value, then check access logs for use of it, then clean history.

Crypto do and do not

Use a vetted high-level library. The list below is the short version of current guidance, and every "do not" row is something that has caused real breaches.

DoDo notWhy
AES-256-GCM, ChaCha20-Poly1305AES-ECBECB encrypts identical blocks identically, so it leaks structure
AEAD (GCM, Poly1305, SIV)CBC without a separate MACUnauthenticated ciphertext is malleable, and padding behavior leaks
A random nonce per message, never reusedA fixed or counter-reset nonceReusing a GCM nonce with the same key breaks confidentiality and forgeability
HMAC-SHA-256 for integrityA bare hash of key + messageNaive constructions are vulnerable to length extension
SHA-256, SHA-384, SHA-512, SHA-3MD5, SHA-1Both are broken for collision resistance
Argon2id, scrypt, bcrypt, PBKDF2 for passwordsSHA-256 for passwordsFast hashes are the wrong tool, see the authentication topic
HKDF to derive keys from a master secretUsing one key for everythingKey separation limits the damage of any single exposure
secrets / crypto.randomBytes / SecureRandomrandom, Math.random, rand()Non-cryptographic PRNGs are predictable from a few outputs
Constant-time comparison== on a secret or a MACByte-by-byte comparison leaks position through timing
Ed25519 or ECDSA P-256 for signatures, RSA ≥ 3072RSA-1024, DSABelow current strength expectations
TLS 1.2 minimum, prefer TLS 1.3SSLv3, TLS 1.0, TLS 1.1Deprecated and vulnerable
A maintained library (libsodium, Tink, cryptography, WebCrypto)Your own constructionRolling your own is the single most reliable way to get this wrong
# Python, cryptography: AES-GCM done correctly
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)     # store in a KMS, not in code
aead = AESGCM(key)
nonce = os.urandom(12)                        # fresh 96-bit nonce PER message
ct = aead.encrypt(nonce, plaintext, associated_data=b"v1:user:42")
pt = aead.decrypt(nonce, ct, associated_data=b"v1:user:42")
// Node, AES-256-GCM with the auth tag kept
import crypto from 'node:crypto'
const iv = crypto.randomBytes(12)
const c = crypto.createCipheriv('aes-256-gcm', key, iv)
const ct = Buffer.concat([c.update(pt), c.final()])
const tag = c.getAuthTag()          // store iv + tag + ct together

The nonce is not a secret, so store it alongside the ciphertext. It must be unique per key, which is a different requirement from being secret.

Constant-time comparison

Comparing a submitted token or MAC with == returns as soon as bytes differ, which leaks how many leading bytes matched. Use the platform's constant-time function.

import hmac
hmac.compare_digest(provided, expected)          # Python
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))   // Node, equal lengths
subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1    // Go
java.security.MessageDigest.isEqual(a, b);               // Java

timingSafeEqual throws when the lengths differ, so compare fixed-length digests, or hash both sides first.

Data in transit and at rest

In transit. TLS everywhere, including internal service-to-service calls.

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

- Redirect all HTTP to HTTPS, then let HSTS remove the redirect from the picture. - Verify certificates. Disabling verification, in any client, is not a workaround. - TLS 1.2 as the floor, TLS 1.3 preferred, and no compression. - preload submits to the browser preload list. It is effectively hard to reverse, so do not add it until every subdomain is HTTPS-only.

At rest. Encrypt with a managed key where the platform offers it, and be honest about what it protects. Full-disk and volume encryption defends against a stolen disk, not against a SQL injection that reads the table.

DataHandling
PasswordsArgon2id or equivalent, never encrypted (you never need to recover them)
Payment card dataDo not store it. Use a PCI-compliant provider's token.
API keys you issueStore a hash, show the value once, and display a prefix for identification
PIIField-level encryption or tokenization where feasible, and minimize what you keep
BackupsEncrypted, with the key stored separately, and restores tested

Hashing something is not encrypting it and encrypting something is not hashing it. Use a hash when you never need the original back, and an AEAD cipher when you do.

Key management

The key is now the secret, so the same rules apply to it, with extra care.

RuleDetail
Never in source or a config file in the repoKMS, HSM, or a secret manager
One key per purposeSeparate encryption, signing, and session keys
Support two keys at onceDecrypt with old and new during rotation, encrypt with new
Version your ciphertextA prefix such as v2: makes migration and rotation possible
Rotate on a schedule and on suspicionAnd test the rotation path before you need it
Audit key accessWho read which key, when
Envelope encryption for bulk dataA KMS key wraps a per-record data key
stored value:  v2:<key-id>:<base64 nonce>:<base64 ciphertext+tag>

Encoding the version and the key id in the stored value is what makes rotation a background job rather than an outage.

Recognizing it in review

grep -rniE "aws_secret|api[_-]?key\s*=\s*[\"'][A-Za-z0-9/+]{16,}" .
grep -rniE "BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY" .
grep -rniE "\bMD5\b|\bSHA1\b|ECB|DES\b|RC4" src/
grep -rniE "Math\.random|random\.randint.*token|rand\(\)" src/
grep -rniE "verify=False|rejectUnauthorized: *false|InsecureSkipVerify" src/

Checklist:

- No secret is in the repository, in a client bundle, in an image layer, or in a log. Secret scanning runs in CI. - Every credential is least privilege, and short-lived where the platform allows it. - Encryption is AEAD, and the nonce is fresh per message. - No MD5, SHA-1, ECB, DES, or RC4 in any security-relevant path. - Randomness for tokens, ids, nonces, and salts comes from a CSPRNG. - Every secret or MAC comparison is constant time. - Certificate verification is enabled in every client, with no verify=False. - HSTS is set with a long max-age, and preload only when every subdomain is ready. - Keys live in a KMS or secret manager, are separated by purpose, are versioned in the stored format, and have a tested rotation runbook. - No custom cryptographic construction exists anywhere in the codebase.

Further reading: the OWASP Cryptographic Storage Cheat Sheet, Secrets Management Cheat Sheet, and Transport Layer Security Cheat Sheet.