Structured analysis of who could attack, how, and what to protect.
STRIDE
Microsoft's 6-category threat taxonomy. Walk every component, asset, and trust boundary through each category.
Letter
Threat
Defense property
S
Spoofing identity
Authentication
T
Tampering with data
Integrity
R
Repudiation
Non-repudiation (signed audit log)
I
Information disclosure
Confidentiality
D
Denial of service
Availability
E
Elevation of privilege
Authorization
STRIDE × component matrix
Data Flow Diagrams & Trust Boundaries
Draw external entities, processes, data stores, data flows. Trust boundaries cross where credentials change (browser→server, service→DB, public→private VPC). Every boundary crossing gets a STRIDE pass.
Simple web-app threat model (data flow + trust boundaries)
Attack Trees
Root = attacker's goal. Children = necessary conditions (AND/OR). Leaves = concrete attacks. Use to identify the cheapest path for the attacker and harden that first.
Attack tree: steal admin credentials
MODULE 2
Cryptography Essentials
What to use, when. Never roll your own.
Symmetric Encryption
Same key encrypts and decrypts. Use for data at rest and any large payload. Modern pick: AES-256-GCM or ChaCha20-Poly1305 — both are AEAD (authenticated encryption with associated data), so they encrypt AND integrity-check.
# Python: correct AES-GCM with random nonce
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aes = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce — NEVER reuse with same key
ct = aes.encrypt(nonce, b"secret", associated_data=b"context-v1")
pt = aes.decrypt(nonce, ct, b"context-v1")
Asymmetric (Public-Key)
Different keys for encrypt/decrypt or sign/verify. Slow → use for key exchange and signatures, not bulk data.
RSA-2048+ — widely supported, slow. Avoid for new code.
ECDSA P-256 — fast, standard in TLS / JWT / code signing.
Ed25519 — fastest, simpler impl, deterministic signatures, preferred for new code.
X25519 — key agreement (Diffie-Hellman) counterpart to Ed25519.
Post-Quantum Cryptography (PQC)
Classical RSA/ECC fall to Shor's algorithm on a large quantum computer. NIST finalized the first PQC standards on 2024-08-13:
Integrity: SHA-256 / SHA-3-256 / BLAKE3 — any modern cryptographic hash.
Password storage: use a KDF, not a plain hash. See below.
Content addressing: BLAKE3 for speed; SHA-256 for ecosystem compat.
Key Derivation & Password Hashing
Passwords are low-entropy — a plain SHA-256 falls to GPU brute-force. Use a slow, memory-hard KDF.
Argon2id — modern winner of PHC. Default for new systems. Parameters: m=64 MB, t=3, p=1 is a safe baseline.
scrypt — memory-hard, well supported.
bcrypt — cost 12+. Capped at 72-char password (truncates longer).
PBKDF2-HMAC-SHA-256 — fallback if FIPS required. iter ≥ 600k (2023 OWASP).
# Python: Argon2id
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=64*1024, parallelism=1)
hashed = ph.hash("user-password") # store this
ph.verify(hashed, "user-password") # raises on mismatch
if ph.check_needs_rehash(hashed): # after param bump
hashed = ph.hash("user-password")
Slow-on-purpose: attacker with GPU farm sees O(1) hashes/sec; legitimate login path is fine.
Password verification flow (Argon2id)
HMAC & Signatures
HMAC = keyed-hash MAC. Use for integrity + authenticity when both sides share a symmetric secret (webhooks, cookies, JWT HS256). Never use plain hash(secret + data) — length-extension attacks.
Digital signatures = asymmetric HMAC. Signer holds private key, verifiers hold public. Ed25519 / ECDSA for new code.
alg: "none" — library MUST reject. Historical CVE.
Algorithm confusion — server expects RS256 but attacker sends HS256 signed with the server's public key treated as secret. Pin algorithm; don't trust header alg.
Weak HS256 secret — brute-forceable. Use 256-bit random or switch to RS256/Ed25519.
Long TTL — hours/days without refresh is bad. Access ≤ 15m.
Storing in localStorage — accessible to any XSS. Prefer HttpOnly cookie with SameSite=Lax.
No audience / issuer check — token from service A accepted by service B.
OAuth delegates authorization. Auth-code + PKCE is the right flow for all public clients (SPA, native, mobile). Client credentials for machine-to-machine.
OAuth 2.0 Authorization Code + PKCE
OIDC & SAML
OIDC = OAuth + id_token (JWT with user claims). Modern federation default.
SAML 2.0 = XML-based SSO, still dominant in enterprise. More complex; beware XML signature wrapping attacks. Prefer OIDC for new deployments.
MODULE 4
Authorization
What is the caller allowed to do?
RBAC
Users → Roles → Permissions. Simple, widely supported, doesn't express resource-level rules well ("Alice can edit only her docs").
ABAC
Attribute-based. Policy = function of (subject attrs, resource attrs, action, environment). Expressive; needs a policy engine. Standards: XACML (complex), Rego (OPA), Cedar (AWS).
Relationship-based — who has what relation to what. Scales to billions of tuples. Pattern: tuples (user, relation, object) with indirection via usersets ("user:alice is in group:eng"). Used by Google Drive, GitHub, Figma.
Cause: concatenating untrusted input into a query. Defense: parameterized statements. Never build a query via string formatting.
# Python — parameterized (safe)
cur.execute("SELECT * FROM users WHERE email = %s AND active = %s", (email, True))
# NEVER this:
# cur.execute(f"SELECT * FROM users WHERE email = '{email}'")
For NoSQL (MongoDB), reject operator objects in user input: validate shape with a schema (pydantic, joi). For shell commands, use subprocess.run([...]) with a list, never shell=True.
User input becomes a URL the server fetches. Attacker targets 169.254.169.254 (cloud metadata) or internal services. Defense:
Allow-list of host/port/scheme.
Resolve DNS yourself, reject private-range IPs (RFC1918 + link-local + loopback). Beware TOCTOU — re-resolve and pin.
Disable redirects OR re-check each hop.
Prefer a dedicated egress proxy with metadata-service blocked.
On AWS: enforce IMDSv2 (session-token required) so a blind GET can't read creds.
A01:2025 / A01:2021 IDOR — Insecure Direct Object Reference
Endpoint trusts an object ID from the request without authz check. Defense: every object fetch must check the caller owns/has-access to the object. Don't rely on "UUID is random enough" — that's obscurity, not security.
# FastAPI: correct ownership check
@app.get("/doc/{doc_id}")
def get_doc(doc_id: str, user = Depends(current_user)):
doc = db.get(doc_id)
if doc is None or not can_read(user, doc):
raise HTTPException(404) # don't leak existence
return doc
pickle.loads, yaml.load, Java ObjectInputStream on untrusted input = arbitrary code exec. Use JSON with a strict schema (pydantic, jsonschema). If you must use pickle, sign payloads with HMAC and verify before loading.
Other OWASP Hits
A04:2025 / A02:2021 Crypto Failures — weak TLS config, hard-coded keys. Use Mozilla SSL Config Generator.
A06:2025 / A04:2021 Insecure Design — threat-model at design time (mod1).
A02:2025 / A05:2021 Misconfig — defaults (default creds, open S3 bucket, open CORS). Scan with checkov / tfsec.
A03:2025 / A06:2021 Software Supply Chain Failures — expanded from "Vulnerable & Outdated Components" to cover dependencies, build systems, and distribution. Dep scanning: npm audit, pip-audit, Snyk, Dependabot.
A07:2025 / A07:2021 Auth Failures — see mod3.
A09:2025 / A09:2021 Security Logging & Alerting Failures — structured audit log; can you reconstruct who did what to whom, yesterday?
A10:2025 Mishandling of Exceptional Conditions — new in 2025: improper error handling, failing open, logic errors. Fail closed; don't leak internals in errors.
MODULE 6
Transport Security
TLS 1.3, mTLS, HSTS, cert pinning.
TLS 1.3
1-RTT handshake (0-RTT with PSK). Only AEAD ciphers. Drops RSA key exchange, SHA-1, CBC. See networking mod6 for the handshake timeline.
Post-quantum: the hybrid X25519MLKEM768 named group (X25519 + ML-KEM-768) is now the de-facto default key exchange — enabled by default in Chrome and Firefox and across Cloudflare's edge.
Disable old protocols: SSLv2/3, TLS 1.0, TLS 1.1.
mTLS
Both sides present certs. Used service-to-service in mesh (Istio, Linkerd). Identity = SPIFFE/SPIRE IDs embedded in cert SAN.
Tells browser: only HTTPS for this host for 1 year. Submit to hstspreload.org to bake into browser source — no first-request window.
Cert Pinning & CT
Pinning = app hard-codes accepted cert or public-key hash. Kills MITM via rogue CA but brittle on rotation. Mobile apps common, web is deprecated (HPKP dead).
Certificate Transparency = public append-only log of issued certs. Monitor for unauthorized certs on your domain (crt.sh, Cert Spotter).
TLS certificate chain
MODULE 7
Secrets Management
Keep secrets out of code, config, and logs.
Never Commit Secrets
Pre-commit hook: gitleaks / detect-secrets.
Rotate anything ever committed — even if rolled back, it's in history.
.env files: gitignored, but still unencrypted on disk. OK for dev, NOT prod.
Kubernetes: external-secrets operator syncs from a manager into K8s Secret.
Envelope Encryption
Encrypt data with a DEK (data encryption key). Encrypt the DEK with a KEK held by KMS. Store {ciphertext, wrapped_DEK}. Rotation: re-wrap DEK by KEK rotation — don't re-encrypt data.
Envelope encryption
Rotation
All credentials have a max age. Short-lived tokens via OIDC federation (AWS IRSA, GCP WIF) > long-lived access keys.
Design for overlap: new secret valid before old retired.
Alert on un-rotated secrets past threshold.
MODULE 8
Secure Coding Patterns
Defaults that prevent whole classes of bugs.
Validate Input, Encode Output
Validation is allow-list (positive pattern). Encoding is context-aware: HTML body ≠ HTML attr ≠ URL ≠ JSON ≠ script. Use the framework's escaper for the target context.
Rate Limiting
Token bucket per (IP, account, endpoint). Prevents brute-force + abuse. See system-design mod9 for token-bucket viz.
Dependency Scanning
SCA (Software Composition Analysis) — match lockfiles against CVE DB. Snyk, GitHub Dependabot, npm audit, pip-audit, OSV-scanner.
SAST — static analysis on your source. Semgrep, CodeQL.
DAST — probe the running app. ZAP, Burp.
SBOM — Software Bill of Materials in SPDX/CycloneDX format.
Supply-Chain Integrity
SCA/SAST/SBOM tell you what is in your build; supply-chain integrity proves the artifact you run is the one your build actually produced — untampered, from a trusted source. OWASP Top 10:2025 promotes this to its own category (A03:2025 Software Supply Chain Failures), and the xz-utils backdoor (CVE-2024-3094) is the canonical cautionary tale.
SLSA — provenance levels
SLSA (Supply-chain Levels for Software Artifacts) v1.0 defines Build levels that attest how an artifact was built:
Build L1 — provenance exists (build platform, process, top-level inputs). May be unsigned: catches mistakes, not tampering.
Build L2 — built on a hosted platform that emits signed provenance, so consumers can authenticate its origin.
Build L3 — hardened, isolated builds; signing key is unreachable from user-defined build steps, so a malicious build job can't forge provenance.
Signing & attestation (sigstore / cosign)
cosign signs container images and arbitrary artifacts. Keyless signing avoids long-lived keys: it exchanges a CI OIDC token for a short-lived cert from Fulcio, signs, and records the signature + cert in Rekor, an append-only transparency log. Verifiers then bind the signature to a specific identity (e.g. the GitHub Actions workflow that built it).
# Verify an image is signed by the expected CI identity (cosign keyless)
cosign verify \
--certificate-identity-regexp '^https://github.com/acme/.+/.github/workflows/.+@refs/tags/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/acme/app:1.4.2
# Verify a signed SBOM attestation attached to the image
cosign verify-attestation --type spdxjson \
--certificate-identity-regexp '^https://github.com/acme/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/acme/app:1.4.2
Pin everything: lockfiles + hashes
A signature only helps if you fetch the exact bytes it covers. Pin transitive deps to content hashes, not version ranges — then a swapped upstream artifact fails the install.
# requirements.txt with hash pinning (pip refuses any mismatch)
requests==2.32.3 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# install: every package MUST carry a verified hash
pip install --require-hashes -r requirements.txt
Safe Deserialization
JSON only, with a schema. Never pickle.loads or yaml.load on untrusted data. YAML: use yaml.safe_load.
Logging Hygiene
Never log: passwords, tokens, full PAN, full SSN, keys, session cookies.
Mask or hash sensitive fields. Include request ID for trace correlation.
Structured JSON logs; centralize in SIEM.
MODULE 9
Network & Zero Trust
Don't trust the network — authenticate every request.
Beyond Perimeter
Legacy model: hard shell (firewall) around soft interior. Breaks once attacker is inside. Google's BeyondCorp model: every request authenticated + authorized regardless of origin network.
Zero Trust Primitives
Identity-aware proxy (IAP) between client and app. Terminates TLS, authenticates user via OIDC, forwards identity headers to backend.
Service mesh mTLS (Istio, Linkerd) — every intra-cluster call authenticated + encrypted by the data-plane proxy.
Workload identity — SPIFFE SVIDs for services; no shared secrets.
Egress control — explicit allow-list of external domains a workload may reach.
BeyondCorp-style request flow
Network Segmentation
Even in zero-trust, minimize blast radius: separate VPCs per environment (prod/stage/dev), private subnets for data stores, PrivateLink for cross-account. Public subnet only for LB/NAT.
MODULE 10
Interview Cheat Sheet
Questions you WILL be asked.
"How does HTTPS work?"
Client hello → server hello + cert chain → client verifies chain against trust store → ECDHE key agreement → derive session keys → AEAD cipher (TLS 1.3 = 1-RTT). See networking mod6.
"Design an auth system."
Identify users, workloads, and federation. Argon2id for passwords + WebAuthn for MFA. Short-lived access JWT (RS256, 15 min) + opaque refresh token stored server-side. OAuth PKCE for public clients. Session revocation via refresh-token blacklist or JWT + short TTL.
"How to store passwords?"
Argon2id(m=64MB, t=3, p=1) — library stores salt inside hash. Never plain hash. Rate-limit login. Check against HIBP on set.
External-secrets operator → fetches from Vault/Secrets Manager → K8s Secret → projected as env/volume. Pod uses IRSA/WIF for manager auth — no shared keys. Rotate automatically.
"How do you design a rate limiter?"
Token bucket per (key). Redis with INCR + EXPIRE, or Envoy local rate limit + global. Burst = bucket size; steady = refill rate. See system-design mod9.
"Encrypt data at rest — envelope encryption why?"
Symmetric DEK fast for bulk; KEK in KMS never leaves the boundary. Rotation = re-wrap DEK, no data re-encryption. Cheap audit: KMS logs every unwrap.
MODULE 11
API Security, Cloud/K8s Hardening & Detection/Response
The OWASP API Top 10, cloud/container hardening, and the blue-team side.
OWASP API Top 10 & BOLA: the #1 API killer
Web app security assumes a browser + server-rendered pages; APIs invert that — the client is untrusted code you don't control, and every object is addressable by ID. The OWASP API Security Top 10 (2023) exists because classic WAFs and the web Top 10 miss API-specific flaws. The worst by far is BOLA (Broken Object-Level Authorization, API1): the endpoint authenticates who you are but never checks whether this user may touch this object.
BOLA (API1) — GET /api/orders/1043 works; change to 1044 and you read another tenant's order. AuthN passed, object-level AuthZ missing.
Broken Authentication (API2) — weak JWT validation (alg:none, no signature check), credential stuffing, no lockout.
BOPLA (API3) — Broken Object Property-Level Auth = mass assignment (write extra fields) + excessive data exposure (read extra fields).
BFLA (API5) — Broken Function-Level Auth: a regular user calls an admin-only route because access is enforced only in the UI.
Unrestricted Resource Consumption (API4) — no rate/size/cost limits → DoS and cloud bill blowups.
The fix for BOLA is never trust an ID from the client as proof of ownership. Re-derive authorization from the session on every request, scoped by tenant:
# VULNERABLE: any authenticated user can read any order
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user=Depends(auth)):
return db.query(Order).get(order_id) # no ownership check!
# SAFE: object lookup is *scoped* to the caller, 404 (not 403) to avoid enumeration
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user=Depends(auth)):
order = db.query(Order).filter(
Order.id == order_id,
Order.tenant_id == user.tenant_id, # authz baked into the query
).first()
if order is None:
raise HTTPException(404) # don't leak existence
return order
Mass assignment & broken function-level authorization
Mass assignment (part of API3/BOPLA) happens when a framework auto-binds the whole request body onto a model. The client sends fields the developer never intended to be writable — role, is_admin, balance, verified — and the ORM dutifully saves them. BFLA (API5) is its function-level cousin: guessing or reading the admin route (POST /api/admin/users) and calling it directly because authorization lives only in the frontend menu.
Flaw
Attacker action
Root cause
Defense
Mass assignment
Add "role":"admin" to signup JSON
Bind-all model / no field allow-list
Explicit input schema (DTO) with allow-listed fields
Excessive exposure
Read password_hash, ssn in response
Serialize entire ORM object
Explicit output schema; never return model.__dict__
BFLA
Call DELETE /admin/users/5 as normal user
UI-only gating; verb/path not checked server-side
Server-side role check per route; deny-by-default
# VULNERABLE: Pydantic/ORM binds everything the client sends
user = User(**request.json()) # {"email":..,"password":..,"is_admin":true}
# SAFE: separate INPUT schema (allow-list) from the DB model
class SignupIn(BaseModel):
email: EmailStr
password: SecretStr # is_admin / role simply do not exist here
user = User(email=data.email, password_hash=hash(data.password), role="user")
# BFLA defense: deny-by-default decorator on every admin route
@app.delete("/api/admin/users/{uid}")
@require_role("admin") # enforced in the API, not the SPA
def delete_user(uid: int): ...
GraphQL abuse & rate-limit bypass
GraphQL replaces many fixed REST endpoints with one flexible query engine — which moves the attack surface into query shape. A single request can demand deeply nested, cyclic, or fan-out data that costs the server orders of magnitude more than it costs the client to send. REST rate limits (requests/minute) are meaningless when one request does the damage of ten thousand.
Deep/nested query DoS — cyclic relations (author → posts → author → posts…) explode work exponentially.
Introspection leakage — __schema hands attackers your full type graph, including hidden mutations. Disable in prod.
Batching / aliasing bypass — 1 HTTP request with 1000 aliased mutations bypasses per-request rate limits (a favorite for brute-forcing OTP/login).
Field-level authz gaps — resolvers that trust the parent object skip per-field checks (GraphQL BOLA).
# Alias-based rate-limit bypass: 1 request = 1000 login attempts
mutation {
a1: login(user:"admin", pass:"aaaa") { token }
a2: login(user:"admin", pass:"aaab") { token }
# ... a1000: login(...) one HTTP POST, one "request" to a naive limiter
}
Defenses stack: cap query depth and complexity (cost analysis), limit aliases/batch size, disable introspection, and rate-limit by cost not by request count.
// graphql-armor / envelop style guards
import { costLimit, maxDepth, maxAliases } from '@escape.tech/graphql-armor';
const plugins = [
maxDepth({ n: 7 }), // reject queries deeper than 7 levels
maxAliases({ n: 15 }), // kill the 1000-alias brute force
costLimit({ maxCost: 5000 }), // assign per-field cost, budget the query
];
// prod schema: introspection disabled
const server = new ApolloServer({ schema, introspection: false });
Least-privilege IAM policy design
Cloud breaches are overwhelmingly IAM misconfigurations, not zero-days. The goal is least privilege: grant the minimum actions, on the minimum resources, under the minimum conditions. AWS evaluates policies as explicit deny > explicit allow > implicit deny — default is deny, one Deny overrides any Allow. The classic anti-pattern is "Action":"*","Resource":"*" attached to an app role “to make it work.”
// BAD: god-mode role — a compromised pod owns the account
{ "Effect": "Allow", "Action": "*", "Resource": "*" }
// GOOD: scoped to specific actions, a specific bucket prefix, with guardrails
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "ReadTenantUploads",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::acme-uploads",
"arn:aws:s3:::acme-uploads/${aws:PrincipalTag/tenant}/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "true" },
"StringEquals": { "aws:PrincipalOrgID": "o-1a2b3c" }
}
}]
}
Scope Resource, not just Action — s3:GetObject on * reads every bucket; pin the ARN and prefix.
Conditions as guardrails — aws:SourceIp, aws:PrincipalOrgID, aws:MultiFactorAuthPresent, VPC endpoint conditions.
Permission boundaries + SCPs — cap the maximum any role can be granted, so a developer can't self-escalate to admin.
No long-lived keys — use IAM Roles / IRSA (IAM Roles for Service Accounts) / Workload Identity; rotate and expire via STS.
Right-size from evidence — start deny-all, add actions from CloudTrail “Access Analyzer” last-accessed data, not from guesses.
Kubernetes hardening: RBAC + Pod Security + NetworkPolicy
A default K8s cluster is flat and permissive: pods can talk to every other pod, service accounts often have broad rights, and containers can run as root. Hardening is three orthogonal layers — who can call the API (RBAC), what a pod is allowed to be (Pod Security), and who a pod can talk to (NetworkPolicy).
Control
Threat it stops
Default (unsafe)
Hardened
RBAC
Compromised SA reads secrets / creates pods
Broad roles, cluster-admin bindings
Namespaced Role, verbs+resources allow-listed
Pod Security Admission
root, privileged, hostPath escape
No enforcement
restricted profile enforced per namespace
NetworkPolicy
Lateral movement pod→pod
All pods can reach all pods
Default-deny + explicit allows
# 1) RBAC: least-privilege Role — read pods in ONE namespace, nothing else
kind: Role
metadata: { namespace: payments, name: pod-reader }
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"] # no create/delete, no secrets
---
# 2) Pod Security Admission: enforce the 'restricted' profile
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
pod-security.kubernetes.io/enforce: restricted # blocks root/privileged
pod-security.kubernetes.io/enforce-version: latest
---
# 3) NetworkPolicy: default-deny all ingress in the namespace
kind: NetworkPolicy
metadata: { namespace: payments, name: default-deny-ingress }
spec:
podSelector: {} # selects every pod
policyTypes: ["Ingress"] # with no ingress rules => deny all
# Hardened pod spec matching the 'restricted' profile
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
seccompProfile: { type: RuntimeDefault }
automountServiceAccountToken: false # don't hand every pod an API token
SSRF to the metadata endpoint & robust SSRF defense
SSRF (Server-Side Request Forgery) tricks your server into making requests to attacker-chosen URLs. In the cloud the crown jewel is the instance metadata service (IMDS) at 169.254.169.254 — it hands out temporary IAM credentials to anything on the box. The 2019 Capital One breach was exactly this: SSRF → IMDS → role creds → 100M records from S3.
# The attack: app fetches a user-supplied "image URL"
POST /fetch body: url=http://169.254.169.254/latest/meta-data/iam/security-credentials/app-role
# Response: temporary AccessKeyId / SecretAccessKey / Token -> full S3 access
Enforce IMDSv2 — session-token + PUT required, and set hop-limit to 1 so a container/proxy can't reach it. Kills most SSRF-to-IMDS.
Allow-list, never deny-list — validate the resolved destination against an explicit allowed set; blocking “169.254.*” misses 0177.0.0.1, [::ffff:169.254.169.254], DNS names, and redirects.
Block private ranges — reject RFC1918 (10/8, 172.16/12, 192.168/16), loopback, link-local 169.254/16, and IPv6 equivalents after resolution.
Disable redirects or re-validate each hop; a 302 to 169.254.169.254 defeats front-door checks.
DNS rebinding defeats naive validation: the attacker's domain resolves to a safe public IP when you validate, then re-resolves to 169.254.169.254 milliseconds later when you connect (TOCTOU on DNS). The fix is resolve once, pin, validate the pinned IP, then connect to that exact IP.
import socket, ipaddress
BLOCKED = [ipaddress.ip_network(n) for n in
("10.0.0.0/8","172.16.0.0/12","192.168.0.0/16",
"127.0.0.0/8","169.254.0.0/16","::1/128","fc00::/7")]
def safe_resolve(host: str) -> str:
# resolve ONCE, validate, then connect to THIS ip (no second lookup)
ip = socket.getaddrinfo(host, None)[0][4][0]
addr = ipaddress.ip_address(ip)
if any(addr in net for net in BLOCKED):
raise ValueError(f"blocked target {ip}") # stops rebinding + IMDS
return ip # caller connects to pinned ip, Host header = original host
Detection & response: SIEM, WAF/DDoS, and the IR lifecycle
Prevention fails eventually — detection and response bound the damage. A SIEM (Security Information & Event Management: Splunk, Elastic SIEM, Sentinel) ingests logs from every layer, normalizes them, correlates across sources, and fires alerts. The metric that matters is MTTD/MTTR (mean time to detect/respond): the industry median dwell time was historically 200+ days — good detection cuts that to hours.
Anomaly / UEBA — baseline normal (login geo/time, request volume, data egress) and alert on deviation: impossible-travel logins, a service account suddenly listing all S3 objects.
Log integrity — ship logs off-box to append-only/WORM storage so an attacker can't erase their trail; monitor for gaps.
The incident response lifecycle (NIST SP 800-61) is the muscle memory: Preparation → Detection & Analysis → Containment → Eradication → Recovery → Post-incident (lessons learned). Containment buys time (isolate the host, rotate the leaked credentials); eradication removes the foothold; recovery restores from known-good; the blameless post-mortem prevents recurrence.
Defending TOCTOU races & memory-safety bugs
Two low-level bug classes underpin many escalations. TOCTOU (Time-Of-Check to Time-Of-Use) is a race: you validate a resource, then use it, and an attacker swaps it in the gap. Memory-safety bugs (buffer overflow, use-after-free, integer overflow) let attackers corrupt state or execute code — still ~70% of severe CVEs in C/C++ codebases (Microsoft/Chromium data).
/* VULNERABLE TOCTOU: check then use, attacker swaps path for a symlink between */
if (access("/tmp/job", W_OK) == 0) { /* TIME OF CHECK */
fd = open("/tmp/job", O_WRONLY); /* TIME OF USE -> now points at /etc/passwd */
write(fd, data, len);
}
/* SAFE: operate on the handle atomically, no re-resolution of the name */
fd = open("/tmp/job", O_WRONLY | O_NOFOLLOW | O_CREAT | O_EXCL, 0600);
if (fd >= 0) { /* opened THIS file exclusively; no window to swap */ }
Beat TOCTOU — collapse check+use into one atomic operation: open the fd then fstat the fd (not the path), use O_NOFOLLOW/O_EXCL, openat with dir handles; for DB/state use transactions with row locks or compare-and-swap, not read-then-write.
Memory safety by construction — prefer memory-safe languages (Rust, Go, Java) for new code; the borrow checker eliminates use-after-free/data races at compile time.
Harden legacy C/C++ — ASan/UBSan in CI, fuzzing (libFuzzer/AFL++), -D_FORTIFY_SOURCE=3, stack canaries, ASLR, DEP/NX, bounds-checked containers (std::span, .at()), and never strcpy/gets.
Integer overflow — validate sizes before malloc(n * size); use checked arithmetic (__builtin_mul_overflow, Rust checked_mul).