Security Best Practices
Review and harden application code against the vulnerabilities that actually get exploited: injection, broken authentication, IDOR, XSS, insecure secrets, weak dependencies. This skill is actionable engineering guidance, not a compliance checklist — each vulnerability class maps to a concrete code-level fix, with cheat-sheets for Python, JavaScript/TypeScript, and Go plus a worked secure-login example.
Quick Reference
| Topic |
Reference |
| STRIDE worksheet, trust boundaries, least privilege, DFDs |
threat-modeling.md |
| JWT verification, OAuth2 flows, OIDC, token storage |
jwt-oauth.md |
Core Workflow
- Scope the review. List entry points (HTTP routes, message consumers, CLIs, file processors), sensitive data (PII, credentials, payment data), and trust boundaries. Note the languages/frameworks so the right cheat-sheet applies.
- Threat-model in 5 minutes. Run the STRIDE table below per entry point; mark high-risk flows (auth, uploads, deserialization, outbound HTTP). Full worksheet: threat-modeling.md.
- Walk the OWASP map. Check every applicable row in the table below. Most findings come from three rows: access control (IDOR), injection, and security misconfiguration.
- Verify secure defaults and apply the cheat-sheet. TLS, headers, CORS, cookies, non-leaky errors (Secure Defaults), then grep for your language's dangerous patterns (SQL f-strings, pickle, innerHTML, eval) and fix confirmed hits.
- Report findings. Prioritize by severity × likelihood; give
file:line, the vulnerability class, why it matters, and the fix. Offer to implement the fixes.
Threat Modeling in 5 Minutes
| Letter |
Threat |
Ask |
Example |
Typical fix |
| S |
Spoofing |
Can someone pretend to be another user/service? |
Forged JWT, stolen session |
Strong auth, signed tokens, verify identity |
| T |
Tampering |
Can data be modified in transit or at rest? |
SQL injection, unsigned payloads |
TLS, signatures/HMAC, parameterized queries |
| R |
Repudiation |
Can an action be denied after the fact? |
"I never made that transfer" |
Audit logs, signed receipts |
| I |
Information disclosure |
Can sensitive data leak? |
Stack traces, IDOR, secrets in logs |
Least privilege, redaction, generic errors |
| D |
Denial of service |
Can the service be overwhelmed? |
Expensive queries, no rate limits |
Rate limiting, resource limits, timeouts |
| E |
Elevation of privilege |
Can a user do more than allowed? |
Role escalation, IDOR |
Server-side authorization on every action |
Trust boundaries sit wherever untrusted input crosses into trusted code (HTTP body → DB query, user input → shell, upload → storage, deserialized data → app, browser → API, app → internal services); every crossing is an attack surface. Least privilege: DB app-user gets SELECT/INSERT on its schema, never DROP/admin; run the OS process unprivileged; scoped IAM roles with short-lived credentials; no "admin mode" that bypasses checks.
OWASP Top 10 → Code-Level Fixes
| Vulnerability |
OWASP '21 |
Code-level fix |
| Injection (SQL, NoSQL, command) |
A03 |
Parameterized queries/ORM bindings, no shell string interpolation |
| Broken authentication |
A07 |
argon2id/bcrypt, MFA, login rate limiting, session rotation |
| Sensitive data exposure |
A02 |
TLS in transit, encryption at rest, no plaintext secrets/PII, no hand-rolled crypto |
| Broken access control / IDOR |
A01 |
Server-side object-level checks; never trust client-supplied IDs or roles |
| Cross-site scripting (XSS) |
A03 (injection) |
Context-aware output encoding, CSP, no dangerous sinks |
| Insecure deserialization |
A08 |
Never pickle/yaml.load untrusted data; allowlist formats, signed payloads |
| Logging & monitoring failures |
A09 |
Log auth events, redact secrets/PII, alert on anomalies |
| Server-side request forgery (SSRF) |
A10 |
Allowlist destinations; block link-local and cloud-metadata IPs |
| Security misconfiguration |
A05 |
Secure defaults: headers, CORS, error messages, debug off |
| Vulnerable & outdated components |
A06 |
Lockfiles, pip-audit/npm audit/osv-scanner in CI, SBOM |
Authentication
Password storage
Never store plaintext, reversible encryption, or fast hashes (MD5/SHA-1/SHA-256 — trivially brute-forced). Use a dedicated password-hashing KDF:
# Python (argon2id — preferred)
from argon2 import PasswordHasher
ph = PasswordHasher() # defaults: m=65536, t=3, p=4
stored = ph.hash(password) # store this string
ph.verify(stored, password) # raises on mismatch
import { hash, verify } from "@node-rs/argon2"; // or bcrypt with cost >= 12
const stored = await hash(password, { memoryCost: 65536, timeCost: 3, parallelism: 4 });
await verify(stored, password);
Salt is automatic with these libraries; bcrypt cost ≥ 12; rehash on upgrade from legacy MD5/SHA1 stores.
Sessions, MFA, and rate limiting
- Session ID: 256-bit random (
secrets.token_urlsafe(32) / crypto.randomBytes(32)), never derived from input.
- Cookie:
HttpOnly, Secure, SameSite=Lax (or Strict), __Host- prefix where supported.
- Rotate the session ID on login/privilege change; invalidate server-side on logout; idle timeout (
30 min) + absolute timeout (24 h).
- TOTP via
pyotp (Python) / otplib (Node), mandatory for admin accounts; rate limit login per IP and per account with backoff.
- Identical error messages and comparable work whether the user exists or not (no user enumeration, no timing side-channel).
Authorization
| Model |
What it checks |
Use when |
| RBAC |
Role → permission mapping (admin, editor) |
Coarse-grained, stable roles |
| ABAC |
Attribute/policy evaluation (user, resource, context) |
Fine-grained, policy-driven access |
| Object-level (ACL/ownership) |
The resource belongs to the caller |
Per-record access — always |
IDOR prevention: never trust a client-supplied ID to imply access. Scope every query to the authenticated user and return 404 (not 403) when the record isn't theirs, so you don't create an ID-existence oracle:
def get_invoice(invoice_id: int, user=Depends(get_current_user)):
invoice = db.scalar(select(Invoice).where(Invoice.id == invoice_id,
Invoice.owner_id == user.id))
if invoice is None:
raise HTTPException(404) # same as "not found"
return invoice
Enforce authorization at the service layer, not just the router, on every endpoint — including hidden admin routes, which must still be verified server-side.
JWT, OAuth2, and OIDC
| Need |
Use |
| Web app with a backend and browser clients |
Server-side sessions + httpOnly cookies (revocable, simplest) |
| Stateless API between your own services |
Short-lived JWT (or opaque tokens from an API gateway) |
| Third-party login (Google, GitHub, Okta) |
OAuth2/OIDC authorization code + PKCE |
| SPA / mobile with an external IdP |
OIDC authorization code + PKCE; access token in memory, refresh token rotated via secure channel |
| Machine-to-machine |
OAuth2 client credentials flow |
JWT verification — non-negotiables (full checklist + alg-confusion attack: jwt-oauth.md):
import jwt
payload = jwt.decode(
token,
key=public_key, # from the issuer's JWKS — never hardcoded or ignored
algorithms=["RS256"], # allowlist — never "none" or HS256 with an RSA key
audience="https://api.example.com",
issuer="https://auth.example.com",
options={"require": ["exp", "iat", "nbf"]},
)
- Verify signature, exp, nbf, iss, aud — always. Reject
alg: none. JWTs are signed, not encrypted — never put secrets or PII in the payload.
- Access tokens short-lived (minutes); refresh tokens rotated and revocable server-side. Never store tokens in
localStorage (any XSS reads them); first-party apps use httpOnly cookies; never log tokens or pass them in URLs.
Input Validation & Injection
SQL injection
Never build SQL with concatenation, f-strings, %, or .format — parameterize or use ORM bindings:
# Bad — injectable via `name`
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# Good
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
With ORMs (SQLAlchemy, Prisma, Django) prefer the query-builder; raw SQL only with bound parameters. Beware string-concat into ORM "extra" clauses.
XSS, CSRF, and SSRF
- Encode for the correct context (HTML, attribute, JS, URL). React/Vue auto-escape text; danger is in sinks:
innerHTML, document.write, dangerouslySetInnerHTML, v-html. Add a restrictive CSP (default-src 'self'); sanitize rich HTML server-side (bleach, DOMPurify).
SameSite cookies + anti-CSRF token for state-changing requests; validate Origin/Referer on non-cookie APIs. CORS is a read-policy, not a write-policy — never rely on it to stop CSRF.
- SSRF: scheme + host allowlist; forbid user-supplied URLs to internal services; block link-local (
169.254.169.254 metadata) and private ranges; disable redirects or re-validate each hop.
File uploads & command injection
- Validate extension and MIME and magic bytes (they must agree); random server-side filenames outside the web root; size limits; never execute uploads.
- Never interpolate input into shell strings — pass args as a list, or
shlex.quote if a shell is unavoidable:
# Bad
os.system(f"ffmpeg -i {filename} out.mp4")
# Good
subprocess.run(["ffmpeg", "-i", filename, "out.mp4"], check=True)
Secrets Management
- Never hardcode secrets or commit real
.env values — commit .env.example (placeholders only); .env* in .gitignore.
- Load from environment variables; use a secrets manager beyond trivial cases (AWS Secrets Manager/KMS, Vault, Doppler, 1Password).
- Scan for leaks in CI (
gitleaks detect / trufflehog; GitHub/GitLab secret scanning); rotate on a schedule and immediately on leak; prefer short-lived, auto-rotating credentials.
- Keep secrets out of logs, errors, and stack traces.
Supply Chain Security
- Commit lockfiles (
package-lock.json, yarn.lock, poetry.lock, uv.lock, hashed requirements.txt) for reproducible builds; generate an SBOM with syft/cyclonedx; use Dependabot/Renovate for automated updates.
- Scan dependencies on every PR in CI:
pip-audit (Python), npm audit/pnpm audit (Node), osv-scanner (all), govulncheck (Go) — gate on high/critical.
- Watch for typosquatting and recently-published malicious packages; verify name and maintainer before adding dependencies.
| Strategy |
Use for |
Risk |
Exact pins (==1.2.3, 1.2.3) |
Applications, reproducible deploys |
You own every upgrade |
Ranges + lockfile (^1.2, ~1.2) |
Libraries, fast-moving deps |
Lockfile drift, supply-chain exposure |
Floating (*, no pin) |
Never |
Non-reproducible, poisoned-by-upstream |
Secure Defaults
- TLS everywhere: enforce HTTPS redirect + HSTS; never ship a disabled cert "for local testing".
| Header |
Value |
Prevents |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
SSL stripping, downgrade |
Content-Security-Policy |
default-src 'self' |
XSS |
X-Content-Type-Options |
nosniff |
MIME sniffing |
X-Frame-Options (or CSP frame-ancestors) |
DENY / 'none' |
Clickjacking |
Referrer-Policy |
no-referrer (or strict-origin-when-cross-origin) |
Referrer leakage |
Permissions-Policy |
minimal set |
Feature abuse (camera, geolocation) |
- CORS: reflect only allowlisted origins; never
Access-Control-Allow-Origin: * with Allow-Credentials: true; minimal Allow-Methods. Cookies: HttpOnly; Secure; SameSite=Lax minimum; __Host- prefix for session cookies.
- Errors: log full stack traces server-side; return generic messages (
"Invalid credentials", "Internal server error"). Never expose DB errors, paths, framework versions, or "user not found" vs "wrong password". Debug off in prod (DEBUG=False, NODE_ENV=production).
Language Cheat-Sheets
| Language |
Pitfall |
Why it's dangerous |
Fix |
| Python |
pickle / yaml.load on untrusted data |
Arbitrary code execution |
yaml.safe_load, JSON/allowlisted formats |
| Python |
eval() / exec() |
Code injection |
Parse with ast or a real parser |
| Python |
SQL via f-strings / % / .format |
SQL injection |
Parameterized queries / ORM bindings |
| Python |
MD5/SHA-1 for passwords |
Instant brute-force |
argon2-cffi / bcrypt with cost params |
| Python |
requests.get(user_url) unchecked |
SSRF |
Scheme/host allowlist, block private IPs |
| JS/TS |
innerHTML, document.write, dangerouslySetInnerHTML, v-html |
XSS |
Auto-escaping, sanitize with DOMPurify, CSP |
| JS/TS |
Untrusted deep-merge (Object.assign, lodash.merge) |
Prototype pollution → RCE |
Filter __proto__/constructor, structuredClone |
| JS/TS |
fetch(user_url) / axios with user URL |
SSRF |
Host allowlist, block metadata IPs, validate redirects |
| JS/TS |
eval / new Function |
Code injection |
Avoid; JSON.parse for data |
| JS/TS |
JWT alg: none / missing verification |
Forged tokens |
Allowlist algs, verify exp/aud/iss |
| Go |
fmt.Sprintf into SQL |
SQL injection |
database/sql placeholders: db.Query("... WHERE id = ?", id) |
| Go |
Returning err.Error() / stack traces in HTTP responses |
Information disclosure |
Log internally, return generic 500 |
| Go |
math/rand for tokens/IDs |
Predictable values |
crypto/rand.Read |
| Go |
Default http.Server (no timeouts), InsecureSkipVerify: true |
Slowloris, MITM |
ReadTimeout/WriteTimeout/IdleTimeout; never skip TLS verify |
| Go |
No gosec / govulncheck in CI |
Known vulnerabilities ship |
Add to CI, fix or document findings |
Worked Example: Secure Login (FastAPI)
argon2id hashing, login rate limiting, timing-safe credential check without user enumeration, random sessions, hardened cookies:
import secrets
from datetime import datetime, timedelta, timezone
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
from fastapi import FastAPI, HTTPException, Request, Response
from slowapi import Limiter
from slowapi.util import get_remote_address
ph = PasswordHasher() # argon2id, OWASP params
DUMMY_HASH = ph.hash("not-a-real-password") # timing parity for unknown users
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
@app.post("/login")
@limiter.limit("5/minute") # per-IP rate limit
async def login(request: Request, response: Response, username: str, password: str):
user = db.get_user(username)
stored = user.password_hash if user else DUMMY_HASH
try:
ph.verify(stored, password)
valid = user is not None
except VerifyMismatchError:
valid = False
if not valid:
raise HTTPException(401, detail="Invalid credentials") # generic
session_id = secrets.token_urlsafe(32) # 256-bit random session id
db.create_session(session_id, user.id,
expires_at=datetime.now(timezone.utc) + timedelta(hours=24))
response.set_cookie("session", session_id,
httponly=True, secure=True, samesite="lax", max_age=86_400)
return {"ok": True}
The unknown-user branch verifies a dummy hash so timing doesn't reveal valid usernames; the 401 body is identical for "no such user" and "wrong password". The cookie is HttpOnly (XSS-immune reads), Secure (HTTPS only), SameSite=Lax (CSRF-resistant), and the session row is revocable server-side on logout. For MFA, require a TOTP code from pyotp for admin accounts before issuing the session.
Anti-Patterns to Avoid
- Rolling your own crypto — use audited libraries; never invent algorithms, MACs, or password schemes.
- Storing passwords with fast hashes or reversible encryption — always argon2id/bcrypt.
- Committing secrets — real values in code,
.env, or test fixtures are leaks, not config.
- Trusting client-supplied IDs, roles, or
isAdmin flags — IDOR and privilege escalation by construction.
eval, pickle, or yaml.load on untrusted input — code execution on demand.
- Skipping JWT verification (alg, exp, aud) — forged or expired tokens are as good as no auth.
- Returning stack traces / DB errors to users — information disclosure via the error path.
- Treating this skill as a tick-box checklist — prioritize by what attackers can reach; re-review when code changes.
When to Use / Not Use
Use this skill when:
- Reviewing new or existing Python, JS/TS, or Go code for security issues.
- Writing secure-by-default auth, sessions, or APIs (login flows, JWTs, OAuth2/OIDC).
- Fixing specific vulnerability classes: injection, XSS, IDOR, CSRF, SSRF, insecure deserialization, secrets in code.
- Setting up dependency scanning, lockfiles, SBOMs, or secret scanning in CI.
- Preparing code for a pen test, security audit, or production release ("is this safe to deploy?").
Not for:
- Compliance-only work (SOC 2/ISO 27001 evidence collection, policy documents) — this skill is about code, not certifications.
- Infrastructure-only security (network firewalls, Kubernetes hardening, cloud IAM architecture) — use a platform-specific skill.
- Designing novel cryptography — no skill replaces a cryptographer for new algorithms/protocols.
- General code review with no security angle — use a general code-review skill to avoid noise.
- Malware analysis / reverse engineering — a different tooling and threat model entirely.
1---2name: security-best-practices3description: Practical security review and hardening guidance for application code in Python, JavaScript/TypeScript, and Go. Use this skill whenever the user asks for a security review or audit, wants to harden authentication (password hashing, sessions, JWT, OAuth2/OIDC, MFA), fix OWASP Top 10 issues (injection, XSS, IDOR, CSRF, SSRF, deserialization), threat-model with STRIDE, clean up secrets and .env handling, or set up dependency scanning (pip-audit, npm audit, osv-scanner, gitleaks, SBOM). Also trigger for "make this secure", "is this safe to deploy", bcrypt/argon2id, rate limiting logins, RBAC/ABAC, CSP and security headers, cookie flags, and secure-by-default coding help.4---56# Security Best Practices78Review and harden application code against the vulnerabilities that actually get exploited: injection, broken authentication, IDOR, XSS, insecure secrets, weak dependencies. This skill is **actionable engineering guidance, not a compliance checklist** — each vulnerability class maps to a concrete code-level fix, with cheat-sheets for Python, JavaScript/TypeScript, and Go plus a worked secure-login example.910## Quick Reference1112| Topic | Reference |13|-------|-----------|14| STRIDE worksheet, trust boundaries, least privilege, DFDs | [threat-modeling.md](references/threat-modeling.md) |15| JWT verification, OAuth2 flows, OIDC, token storage | [jwt-oauth.md](references/jwt-oauth.md) |1617## Core Workflow18191. **Scope the review.** List entry points (HTTP routes, message consumers, CLIs, file processors), sensitive data (PII, credentials, payment data), and trust boundaries. Note the languages/frameworks so the right cheat-sheet applies.202. **Threat-model in 5 minutes.** Run the STRIDE table below per entry point; mark high-risk flows (auth, uploads, deserialization, outbound HTTP). Full worksheet: [threat-modeling.md](references/threat-modeling.md).213. **Walk the OWASP map.** Check every applicable row in the table below. Most findings come from three rows: access control (IDOR), injection, and security misconfiguration.224. **Verify secure defaults and apply the cheat-sheet.** TLS, headers, CORS, cookies, non-leaky errors (Secure Defaults), then grep for your language's dangerous patterns (SQL f-strings, pickle, innerHTML, eval) and fix confirmed hits.235. **Report findings.** Prioritize by severity × likelihood; give `file:line`, the vulnerability class, why it matters, and the fix. Offer to implement the fixes.2425## Threat Modeling in 5 Minutes2627| Letter | Threat | Ask | Example | Typical fix |28|--------|--------|-----|---------|-------------|29| S | Spoofing | Can someone pretend to be another user/service? | Forged JWT, stolen session | Strong auth, signed tokens, verify identity |30| T | Tampering | Can data be modified in transit or at rest? | SQL injection, unsigned payloads | TLS, signatures/HMAC, parameterized queries |31| R | Repudiation | Can an action be denied after the fact? | "I never made that transfer" | Audit logs, signed receipts |32| I | Information disclosure | Can sensitive data leak? | Stack traces, IDOR, secrets in logs | Least privilege, redaction, generic errors |33| D | Denial of service | Can the service be overwhelmed? | Expensive queries, no rate limits | Rate limiting, resource limits, timeouts |34| E | Elevation of privilege | Can a user do more than allowed? | Role escalation, IDOR | Server-side authorization on every action |3536**Trust boundaries** sit wherever untrusted input crosses into trusted code (HTTP body → DB query, user input → shell, upload → storage, deserialized data → app, browser → API, app → internal services); every crossing is an attack surface. **Least privilege**: DB app-user gets `SELECT`/`INSERT` on its schema, never `DROP`/admin; run the OS process unprivileged; scoped IAM roles with short-lived credentials; no "admin mode" that bypasses checks.3738## OWASP Top 10 → Code-Level Fixes3940| Vulnerability | OWASP '21 | Code-level fix |41|---------------|-----------|----------------|42| Injection (SQL, NoSQL, command) | A03 | Parameterized queries/ORM bindings, no shell string interpolation |43| Broken authentication | A07 | argon2id/bcrypt, MFA, login rate limiting, session rotation |44| Sensitive data exposure | A02 | TLS in transit, encryption at rest, no plaintext secrets/PII, no hand-rolled crypto |45| Broken access control / IDOR | A01 | Server-side object-level checks; never trust client-supplied IDs or roles |46| Cross-site scripting (XSS) | A03 (injection) | Context-aware output encoding, CSP, no dangerous sinks |47| Insecure deserialization | A08 | Never `pickle`/`yaml.load` untrusted data; allowlist formats, signed payloads |48| Logging & monitoring failures | A09 | Log auth events, redact secrets/PII, alert on anomalies |49| Server-side request forgery (SSRF) | A10 | Allowlist destinations; block link-local and cloud-metadata IPs |50| Security misconfiguration | A05 | Secure defaults: headers, CORS, error messages, debug off |51| Vulnerable & outdated components | A06 | Lockfiles, `pip-audit`/`npm audit`/`osv-scanner` in CI, SBOM |5253## Authentication5455### Password storage5657Never store plaintext, reversible encryption, or fast hashes (MD5/SHA-1/SHA-256 — trivially brute-forced). Use a dedicated password-hashing KDF:5859```python60# Python (argon2id — preferred)61from argon2 import PasswordHasher62ph = PasswordHasher() # defaults: m=65536, t=3, p=463stored = ph.hash(password) # store this string64ph.verify(stored, password) # raises on mismatch65```6667```javascript68import { hash, verify } from "@node-rs/argon2"; // or bcrypt with cost >= 1269const stored = await hash(password, { memoryCost: 65536, timeCost: 3, parallelism: 4 });70await verify(stored, password);71```7273Salt is automatic with these libraries; bcrypt cost ≥ 12; rehash on upgrade from legacy MD5/SHA1 stores.7475### Sessions, MFA, and rate limiting7677- Session ID: 256-bit random (`secrets.token_urlsafe(32)` / `crypto.randomBytes(32)`), never derived from input.78- Cookie: `HttpOnly`, `Secure`, `SameSite=Lax` (or `Strict`), `__Host-` prefix where supported.79- Rotate the session ID on login/privilege change; invalidate server-side on logout; idle timeout (~30 min) + absolute timeout (~24 h).80- TOTP via `pyotp` (Python) / `otplib` (Node), mandatory for admin accounts; rate limit login per IP **and** per account with backoff.81- Identical error messages and comparable work whether the user exists or not (no user enumeration, no timing side-channel).8283## Authorization8485| Model | What it checks | Use when |86|-------|----------------|----------|87| RBAC | Role → permission mapping (`admin`, `editor`) | Coarse-grained, stable roles |88| ABAC | Attribute/policy evaluation (user, resource, context) | Fine-grained, policy-driven access |89| Object-level (ACL/ownership) | The *resource* belongs to the *caller* | Per-record access — always |9091**IDOR prevention**: never trust a client-supplied ID to imply access. Scope every query to the authenticated user and return 404 (not 403) when the record isn't theirs, so you don't create an ID-existence oracle:9293```python94def get_invoice(invoice_id: int, user=Depends(get_current_user)):95 invoice = db.scalar(select(Invoice).where(Invoice.id == invoice_id,96 Invoice.owner_id == user.id))97 if invoice is None:98 raise HTTPException(404) # same as "not found"99 return invoice100```101102Enforce authorization at the service layer, not just the router, on **every** endpoint — including hidden admin routes, which must still be verified server-side.103104## JWT, OAuth2, and OIDC105106| Need | Use |107|------|-----|108| Web app with a backend and browser clients | Server-side sessions + `httpOnly` cookies (revocable, simplest) |109| Stateless API between your own services | Short-lived JWT (or opaque tokens from an API gateway) |110| Third-party login (Google, GitHub, Okta) | OAuth2/OIDC authorization code + PKCE |111| SPA / mobile with an external IdP | OIDC authorization code + PKCE; access token in memory, refresh token rotated via secure channel |112| Machine-to-machine | OAuth2 client credentials flow |113114**JWT verification — non-negotiables** (full checklist + alg-confusion attack: [jwt-oauth.md](references/jwt-oauth.md)):115116```python117import jwt118payload = jwt.decode(119 token,120 key=public_key, # from the issuer's JWKS — never hardcoded or ignored121 algorithms=["RS256"], # allowlist — never "none" or HS256 with an RSA key122 audience="https://api.example.com",123 issuer="https://auth.example.com",124 options={"require": ["exp", "iat", "nbf"]},125)126```127128- Verify **signature**, **exp**, **nbf**, **iss**, **aud** — always. Reject `alg: none`. JWTs are signed, **not encrypted** — never put secrets or PII in the payload.129- Access tokens short-lived (minutes); refresh tokens rotated and revocable server-side. Never store tokens in `localStorage` (any XSS reads them); first-party apps use `httpOnly` cookies; never log tokens or pass them in URLs.130131## Input Validation & Injection132133### SQL injection134135Never build SQL with concatenation, f-strings, `%`, or `.format` — parameterize or use ORM bindings:136137```python138# Bad — injectable via `name`139cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")140# Good141cursor.execute("SELECT * FROM users WHERE name = %s", (name,))142```143144With ORMs (SQLAlchemy, Prisma, Django) prefer the query-builder; raw SQL only with bound parameters. Beware string-concat into ORM "extra" clauses.145146### XSS, CSRF, and SSRF147148- Encode for the correct context (HTML, attribute, JS, URL). React/Vue auto-escape text; danger is in sinks: `innerHTML`, `document.write`, `dangerouslySetInnerHTML`, `v-html`. Add a restrictive CSP (`default-src 'self'`); sanitize rich HTML server-side (bleach, DOMPurify).149- `SameSite` cookies + anti-CSRF token for state-changing requests; validate `Origin`/`Referer` on non-cookie APIs. CORS is a read-policy, not a write-policy — never rely on it to stop CSRF.150- SSRF: scheme + host **allowlist**; forbid user-supplied URLs to internal services; block link-local (`169.254.169.254` metadata) and private ranges; disable redirects or re-validate each hop.151152### File uploads & command injection153154- Validate extension **and** MIME **and** magic bytes (they must agree); random server-side filenames outside the web root; size limits; never execute uploads.155- Never interpolate input into shell strings — pass args as a list, or `shlex.quote` if a shell is unavoidable:156157```python158# Bad159os.system(f"ffmpeg -i {filename} out.mp4")160# Good161subprocess.run(["ffmpeg", "-i", filename, "out.mp4"], check=True)162```163164## Secrets Management165166- Never hardcode secrets or commit real `.env` values — commit `.env.example` (placeholders only); `.env*` in `.gitignore`.167- Load from environment variables; use a secrets manager beyond trivial cases (AWS Secrets Manager/KMS, Vault, Doppler, 1Password).168- Scan for leaks in CI (`gitleaks detect` / `trufflehog`; GitHub/GitLab secret scanning); rotate on a schedule and **immediately** on leak; prefer short-lived, auto-rotating credentials.169- Keep secrets out of logs, errors, and stack traces.170171## Supply Chain Security172173- **Commit lockfiles** (`package-lock.json`, `yarn.lock`, `poetry.lock`, `uv.lock`, hashed `requirements.txt`) for reproducible builds; generate an SBOM with `syft`/`cyclonedx`; use Dependabot/Renovate for automated updates.174- Scan dependencies on every PR in CI: `pip-audit` (Python), `npm audit`/`pnpm audit` (Node), `osv-scanner` (all), `govulncheck` (Go) — gate on high/critical.175- Watch for typosquatting and recently-published malicious packages; verify name and maintainer before adding dependencies.176177| Strategy | Use for | Risk |178|----------|---------|------|179| Exact pins (`==1.2.3`, `1.2.3`) | Applications, reproducible deploys | You own every upgrade |180| Ranges + lockfile (`^1.2`, `~1.2`) | Libraries, fast-moving deps | Lockfile drift, supply-chain exposure |181| Floating (`*`, no pin) | Never | Non-reproducible, poisoned-by-upstream |182183## Secure Defaults184185- **TLS everywhere**: enforce HTTPS redirect + HSTS; never ship a disabled cert "for local testing".186187| Header | Value | Prevents |188|--------|-------|----------|189| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | SSL stripping, downgrade |190| `Content-Security-Policy` | `default-src 'self'` | XSS |191| `X-Content-Type-Options` | `nosniff` | MIME sniffing |192| `X-Frame-Options` (or CSP `frame-ancestors`) | `DENY` / `'none'` | Clickjacking |193| `Referrer-Policy` | `no-referrer` (or `strict-origin-when-cross-origin`) | Referrer leakage |194| `Permissions-Policy` | minimal set | Feature abuse (camera, geolocation) |195196- **CORS**: reflect only allowlisted origins; never `Access-Control-Allow-Origin: *` with `Allow-Credentials: true`; minimal `Allow-Methods`. **Cookies**: `HttpOnly; Secure; SameSite=Lax` minimum; `__Host-` prefix for session cookies.197- **Errors**: log full stack traces server-side; return generic messages (`"Invalid credentials"`, `"Internal server error"`). Never expose DB errors, paths, framework versions, or "user not found" vs "wrong password". Debug off in prod (`DEBUG=False`, `NODE_ENV=production`).198199## Language Cheat-Sheets200201| Language | Pitfall | Why it's dangerous | Fix |202|----------|---------|--------------------|-----|203| Python | `pickle` / `yaml.load` on untrusted data | Arbitrary code execution | `yaml.safe_load`, JSON/allowlisted formats |204| Python | `eval()` / `exec()` | Code injection | Parse with `ast` or a real parser |205| Python | SQL via f-strings / `%` / `.format` | SQL injection | Parameterized queries / ORM bindings |206| Python | MD5/SHA-1 for passwords | Instant brute-force | `argon2-cffi` / `bcrypt` with cost params |207| Python | `requests.get(user_url)` unchecked | SSRF | Scheme/host allowlist, block private IPs |208| JS/TS | `innerHTML`, `document.write`, `dangerouslySetInnerHTML`, `v-html` | XSS | Auto-escaping, sanitize with DOMPurify, CSP |209| JS/TS | Untrusted deep-merge (`Object.assign`, `lodash.merge`) | Prototype pollution → RCE | Filter `__proto__`/`constructor`, `structuredClone` |210| JS/TS | `fetch(user_url)` / axios with user URL | SSRF | Host allowlist, block metadata IPs, validate redirects |211| JS/TS | `eval` / `new Function` | Code injection | Avoid; `JSON.parse` for data |212| JS/TS | JWT `alg: none` / missing verification | Forged tokens | Allowlist algs, verify exp/aud/iss |213| Go | `fmt.Sprintf` into SQL | SQL injection | `database/sql` placeholders: `db.Query("... WHERE id = ?", id)` |214| Go | Returning `err.Error()` / stack traces in HTTP responses | Information disclosure | Log internally, return generic 500 |215| Go | `math/rand` for tokens/IDs | Predictable values | `crypto/rand.Read` |216| Go | Default `http.Server` (no timeouts), `InsecureSkipVerify: true` | Slowloris, MITM | `ReadTimeout`/`WriteTimeout`/`IdleTimeout`; never skip TLS verify |217| Go | No `gosec` / `govulncheck` in CI | Known vulnerabilities ship | Add to CI, fix or document findings |218219## Worked Example: Secure Login (FastAPI)220221argon2id hashing, login rate limiting, timing-safe credential check without user enumeration, random sessions, hardened cookies:222223```python224import secrets225from datetime import datetime, timedelta, timezone226227from argon2 import PasswordHasher228from argon2.exceptions import VerifyMismatchError229from fastapi import FastAPI, HTTPException, Request, Response230from slowapi import Limiter231from slowapi.util import get_remote_address232233ph = PasswordHasher() # argon2id, OWASP params234DUMMY_HASH = ph.hash("not-a-real-password") # timing parity for unknown users235limiter = Limiter(key_func=get_remote_address)236app = FastAPI()237app.state.limiter = limiter238239@app.post("/login")240@limiter.limit("5/minute") # per-IP rate limit241async def login(request: Request, response: Response, username: str, password: str):242 user = db.get_user(username)243 stored = user.password_hash if user else DUMMY_HASH244 try:245 ph.verify(stored, password)246 valid = user is not None247 except VerifyMismatchError:248 valid = False249 if not valid:250 raise HTTPException(401, detail="Invalid credentials") # generic251252 session_id = secrets.token_urlsafe(32) # 256-bit random session id253 db.create_session(session_id, user.id,254 expires_at=datetime.now(timezone.utc) + timedelta(hours=24))255 response.set_cookie("session", session_id,256 httponly=True, secure=True, samesite="lax", max_age=86_400)257 return {"ok": True}258```259260The unknown-user branch verifies a dummy hash so timing doesn't reveal valid usernames; the 401 body is identical for "no such user" and "wrong password". The cookie is `HttpOnly` (XSS-immune reads), `Secure` (HTTPS only), `SameSite=Lax` (CSRF-resistant), and the session row is revocable server-side on logout. For MFA, require a TOTP code from `pyotp` for admin accounts before issuing the session.261262## Anti-Patterns to Avoid263264- **Rolling your own crypto** — use audited libraries; never invent algorithms, MACs, or password schemes.265- **Storing passwords with fast hashes or reversible encryption** — always argon2id/bcrypt.266- **Committing secrets** — real values in code, `.env`, or test fixtures are leaks, not config.267- **Trusting client-supplied IDs, roles, or `isAdmin` flags** — IDOR and privilege escalation by construction.268- **`eval`, `pickle`, or `yaml.load` on untrusted input** — code execution on demand.269- **Skipping JWT verification** (alg, exp, aud) — forged or expired tokens are as good as no auth.270- **Returning stack traces / DB errors to users** — information disclosure via the error path.271- **Treating this skill as a tick-box checklist** — prioritize by what attackers can reach; re-review when code changes.272273## When to Use / Not Use274275**Use this skill when:**276- Reviewing new or existing Python, JS/TS, or Go code for security issues.277- Writing secure-by-default auth, sessions, or APIs (login flows, JWTs, OAuth2/OIDC).278- Fixing specific vulnerability classes: injection, XSS, IDOR, CSRF, SSRF, insecure deserialization, secrets in code.279- Setting up dependency scanning, lockfiles, SBOMs, or secret scanning in CI.280- Preparing code for a pen test, security audit, or production release ("is this safe to deploy?").281282**Not for:**283- **Compliance-only work** (SOC 2/ISO 27001 evidence collection, policy documents) — this skill is about code, not certifications.284- **Infrastructure-only security** (network firewalls, Kubernetes hardening, cloud IAM architecture) — use a platform-specific skill.285- **Designing novel cryptography** — no skill replaces a cryptographer for new algorithms/protocols.286- **General code review with no security angle** — use a general code-review skill to avoid noise.287- **Malware analysis / reverse engineering** — a different tooling and threat model entirely.