Cheat sheet · Security for Developers: Web App Security From Zero

Security for Developers cheat sheet

The attacks and the fixes for a web app, from injection and XSS to passwords, secrets, supply chain and incidents. One rule runs through all of it: everything from outside your code is untrusted, so keep data away from code.

Read the full series →

blogs · jp-blogs.vercel.app/cheatsheets/series/security

The security mindsetLesson 0 →

  • Attackers send the unexpected: a fourth dropdown value, /notes/18, a fake X-User-Id, a huge input.
  • Defence in depth: layers make one bug smaller. A read-only DB user stopped writes despite injectable SQL.
  • Least privilege and fail closed: smallest power that works, and an error means no.
  • STRIDE: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation.
  • Threat model: what are we building, what can go wrong, what will we do, did we do enough.

OWASP Top 10:2025Lesson 0 →

#Category
A01Broken Access Control (includes SSRF, CSRF)
A02Security Misconfiguration
A03Software Supply Chain Failures
A04Cryptographic Failures
A05Injection (SQL, OS, NoSQL, XSS)
A06Insecure Design
A07Authentication Failures
A08Software or Data Integrity Failures
A09Security Logging & Alerting Failures
A10Mishandling of Exceptional Conditions

Watch out: Name the edition. "A03 is Injection" was the 2021 numbering. In 2025 Injection is A05.

Injection: never mix data into codeLesson 1 →

// BAD: name = admin'--  logs in as admin
`... WHERE name = '${name}'`

// GOOD: SQL and values travel separately
db.prepare(
  "SELECT id FROM users
  WHERE name = ? AND pw = ?"
).get(name, pw);

Escaping quotes is a weak fix. Use the interface made to carry data.

InterpreterAttackData channel
SQLadmin'--, ' OR '1'='1, UNION? parameters
ShellSam; echo INJECTEDexecFile(prog, [args]), -- before values
MongoDB{"password":{"$ne":null}}Check typeof is string at the edge
Sort column?sort=name; DROP...Allow-list, Object.hasOwn
  • A plain-object lookup of constructor returns an inherited function. Use Object.hasOwn or a Map.
  • exec runs ; | &&. execFile does not, but -la can still be an option. Never shell: true.

XSS and CSPLesson 2 →

  • Reflected: payload in the link. Stored: saved, hits every visitor. DOM: page JS writes it, e.g. innerHTML = location.hash.
  • Fix: encode output for the place it lands (HTML text, attribute, JS string, URL, CSS each differ). < becomes &lt;.
  • HttpOnly hides the cookie, but the script can still act as the user. It reduces damage only.
FrameworkSafe by defaultThe hole
React{value}dangerouslySetInnerHTML
Vue{{ value }}v-html
Plain JStextContentinnerHTML
default-src 'self'
Load things from my origin only
script-src 'self'
No inline scripts. Use a nonce or hash to allow one
frame-ancestors 'none'
Nobody may frame the page
Content-Security-Policy-Report-Only
Log what would break, block nothing

Watch out: CSP is a net, not a cure. 'unsafe-inline' or https: wildcards give almost no protection.

CSRF, clickjacking, headersLesson 2 →

  • CSRF: the browser attaches your cookie to a request another site started. The attacker cannot read the reply, only trigger the action.
  • Defence: explicit SameSite, a random session-bound token, check Origin or Sec-Fetch-Site. GET never changes data.
SameSiteCookie sent when
StrictRequest starts on your own site only
LaxAlso top-level link navigation (GET). Not cross-site POST
NoneAlways (needs Secure)
frame-ancestors / X-Frame-Options
Stop clickjacking (Chromium: ERR_BLOCKED_BY_RESPONSE)
Strict-Transport-Security
HTTPS only. Only send over HTTPS
X-Content-Type-Options: nosniff
No file-type guessing

Tip: helmet() sets a sensible group. Cookie flags are yours. Check the real response with curl -I.

Storing passwordsLesson 3 →

  • Never store or encrypt the password. Store a slow, salted hash. A salt is random, per user, not secret.
  • SHA-256 is fast: about 330,000 guesses per second on one JS thread. Same password gave the same hash for alice and dave.
  • Same error for every login failure: "Email or password is incorrect". Run a dummy hash for unknown users.
FunctionOWASP minimum
Argon2id (first choice)19 MiB memory, 2 iterations, 1 parallelism
scryptN=2^17, r=8, p=1. Set maxmem or Node throws
bcrypt (legacy)Work factor 10+, 72 byte limit
PBKDF2 (FIPS)600,000+ iterations, HMAC-SHA-256

Tip: Rules that help: 8+ chars with MFA, allow 64+, any character, block breached passwords, no forced rotation.

Sessions and JWTLesson 3 →

Set-Cookie: __Host-sid=<randomBytes(32)>;
  Path=/; Secure; HttpOnly;
  SameSite=Lax; Max-Age=3600

New ID at login (stops fixation). Idle and absolute timeouts. Never in localStorage.

  • __Host- forces Secure, Path=/, no Domain. Secure does nothing if the site also serves plain HTTP: redirect and use HSTS.
  • A JWT payload is base64url, readable by anyone. The signature only stops edits. No secrets inside.
  • Verify: pin the algorithm in code (reject alg: none), require exp, check iss and aud, use a 32+ byte secret.

Reset, MFA and guessing limitsLesson 3 →

  1. Same message whether the email exists or not.
  2. Token: 32 random bytes, store only its SHA-256, single use, about 15 minutes.
  3. On use: mark used, end other sessions, email "password changed". Never email the password.
MFA methodWeak spot
TOTP app (RFC 6238)Phishable within 30 s. Reject a replayed code
SMS or callSIM swap. OWASP: restricted
Passkey / FIDO2Phishing resistant. Needs recovery plan
Recovery codesSingle use, stored hashed
  • Rate limit per account and per IP. Prefer growing delays to hard locks. Check the limiter before hashing.
  • Credential stuffing: breached-password block, MFA, one error text, alert on failure spikes.

Access control (IDOR)Lesson 4 →

-- owner id comes from the SESSION
SELECT * FROM notes
 WHERE id = $1 AND owner_id = $2;

Login passed but nobody asked "is it yours?" That is an IDOR (BOLA in APIs).

  • Check every method (read, update, delete, export), on the server, in one central layer. Hidden buttons are not security.
  • Deny by default: no matching rule means no (?? false). Random UUIDs are a bonus, not the lock.
  • Mass assignment: Object.assign(me, body) lets role: admin in. Allow-list fields or use a DTO, and keep the owner check.
CodeUse when
401Not logged in
403Known user, no. Existence is public
404Existence is private: "not yours" looks like "not there"

Crypto, randomness, TLSLesson 5 →

ToolKey?Reversible?Use
HashNoNoFingerprint, slow hash for passwords
EncodingNoYes, anyonebase64 is not security
HMACYesNoWebhook signs, JWT HS256
EncryptionYesWith keyAES-256-GCM for data to read back
  • GCM: new random 12-byte IV every time, store IV + 16-byte tag + ciphertext. A flipped bit throws.
  • Secrets use randomBytes, randomUUID, randomInt. Never Math.random.
  • TLS gives identity, privacy, integrity. Never set rejectUnauthorized: false. HSTS: short max-age first.

A secret leaked: what nowLesson 5 →

  1. Rotate now: new key, deploy, revoke the old one.
  2. Check the provider logs and billing for use.
  3. Remove from code, load from a secret store.
  4. Add scanning and push protection.
  • Deleting .env in the next commit changes the latest snapshot only. git log -p still shows it.
  • NEXT_PUBLIC_ values are inlined into the browser bundle. Public by design.

Dependencies and CILesson 6 →

npm ci
Exact lock file tree. Fails if out of sync
ignore-scripts=true
In .npmrc: no silent postinstall
npm audit --audit-level=high
Exit 1 on high or critical. Known issues only
  • Entry routes: typosquatting, install scripts, account takeover, new owner (event-stream), dependency confusion (use @scope/).
  • One npm install express brought 65 packages. Read lock file diffs for odd resolved hosts.
permissions:
  contents: read
# pin to a full SHA, not a tag
- uses: actions/checkout@<40-char-sha>
  with: { persist-credentials: false }
# untrusted text via env, not run:
- env:
    TITLE: ${{ github.event.pr.title }}
  run: echo "$TITLE"

Avoid pull_request_target with PR code. tj-actions moved its tags in 2025.

SSRF and file uploadsLesson 7 →

  • SSRF: your server fetches an attacker URL and reaches 127.0.0.1, internal hosts, or 169.254.169.254 (cloud metadata).
  • An allow-list still leaked through a redirect. Use redirect: "manual" or re-check each hop. Allow only http: and https:.
  • Open URLs: resolve, then check the IP, inside the connection's own lookup (stops DNS rebinding). ::ffff:7f00:1 is loopback.
Upload riskDefence
../victim.txt nameGenerate the name (randomUUID() + ext). Check startsWith(dir + sep)
shell.jsExtension allow-list, 415 otherwise
Fake Content-TypeIgnore it. Check magic bytes. Re-encode images
Huge bodyStop reading at the limit, 413. Set proxy limit, quota

Errors, CORS, rate limitsLesson 7 →

  • Errors: log the stack under an error ID, return only { error, errorId }. Debug off in production. Do not fail open.
  • Limiter: return 429 with Retry-After. In-memory counts are per process, so use Redis or the gateway. Limit sizes too (?limit=).
  • CORS is a browser rule that relaxes same-origin. It is not authentication and does not stop CSRF.
CORS mistakeWhy bad
Reflect Origin + credentialsAny site reads logged-in data
Allow nullSandboxed pages send it
endsWith("example.com")evilexample.com passes. Compare exactly

Tip: OWASP API Top 10: API1 BOLA, API3 field level, API5 function level, API4 no limits, API7 SSRF.

Review, scanners, incidentLesson 8 →

ScannerMisses
SAST (Semgrep, CodeQL)A missing owner check: no pattern to match
DAST (OWASP ZAP)Code no request reaches; logic bugs
SCA (npm audit)Brand-new malicious versions
Secrets (gitleaks)Shapeless secrets. gitleaks git scans history
  1. Declare, name one lead and a note-taker.
  2. Contain: revoke, disable, roll back. Do not delete evidence.
  3. Rotate every secret that may be exposed.
  4. Save logs and snapshots with times.
  5. Scope: first bad event, what was touched.
  6. Tell owners (GDPR can mean 72 hours), then fix with a test.

Tip: In review, hunt new doors (input, route, outbound call, package) and deleted checks. Write the test with the fix.

Interview one-linersLesson 0 →

QuestionShort answer
SQL injection fix?Parameterised queries; allow-list names; least-privilege DB user.
Why does CSRF work?Cookie is sent whoever starts the request. SameSite plus token.
Store passwords?Argon2id or scrypt, unique salt, tuned cost.
Do UUIDs fix IDOR?No. Check ownership on the server.
Key committed?Rotate first, check logs, then clean up.
We use HTTPS, so safe?No. It protects the pipe, not the endpoints.

A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.