Hitchhiker's Guide — Hardening Web, API, Identity, and TLS

The operational companion to the Phase 02 WARMUP. The WARMUP builds the concepts (the request chain, authN vs authZ, OAuth/OIDC/JWT, the web attack classes, AEAD/KMS); this guide is the how — the attack lab, the table-driven negative authorization matrix, identity and TLS drills, chained-failure analysis, and the production proof that authorization fails closed. All targets are owned lab applications with synthetic data.

Table of Contents


Reference Service Architecture

browser/client ─→ edge proxy ─→ application ─→ policy decision ─→ data service
                       │              │               │
                    TLS logs       audit ID       object filter (tenant, not caller-controlled)

Stand up: two tenants, human and workload identities, a local OIDC provider, a webhook sender, an outbound-fetch worker, and synthetic sensitive objects. The point is to test identity and policy propagation through hops (Phase 02 WARMUP, Chapter 10), not just individual route handlers — a correct route handler that loses the subject across a service hop is still a confused deputy.

Build an Attack Lab

Seed these abuse cases against the lab and capture request/decision/audit IDs for each:

  • change an object ID to another tenant's (IDOR/BOLA);
  • use a reader token on update/delete;
  • replay an expired token; swap iss or aud;
  • send duplicate parameters and conflicting Content-Types (parser differential);
  • ask the service indexer to read restricted content (confused deputy);
  • upload a traversal / polyglot / oversized fixture;
  • request metadata / a private IP through the outbound-fetch feature (SSRF).

The Negative Authorization Matrix

Authorization tests are table-driven and negative-heavy — most rows should be deny:

cases = [
    # (principal,    action,   resource,      expect_allow)
    ("reader",       "read",   "shared-doc",  True),
    ("reader",       "update", "shared-doc",  False),
    ("admin-t1",     "delete", "tenant2-doc", False),   # cross-tenant
    ("expired-tok",  "read",   "shared-doc",  False),
    ("workload-tok", "read",   "user-route",  False),   # wrong audience/type
]

For every action, test: unauthenticated, wrong role, wrong tenant, disabled account, expired token, wrong issuer/audience/type, stale relationship, and service-to-service misuse. Include search, counts, exports, error paths, and timing — not just direct reads. Assert the policy decision and the audit record, not only the HTTP status (a 403 with the wrong audit reason is still a bug). A new endpoint absent from the matrix must fail CI.

SSRF Hardening Procedure

The outbound-fetch defense, in order (Phase 02 WARMUP, Chapter 11):

  1. canonicalize the URL once with a strict parser;
  2. restrict scheme/port (allowlist https, specific ports);
  3. resolve through a controlled DNS resolver and pin the result;
  4. reject loopback, link-local (169.254.0.0/16), private (RFC1918), and metadata ranges — validate the resolved IP, not the hostname string;
  5. route through an egress proxy with logging;
  6. re-validate on every redirect hop (defeats DNS rebinding and redirect-to-internal).

Prove it with a fixture whose DNS flips from public to 169.254.169.254 between check and connect.

Identity Failure Drills

  • rotate the signing key while clients cache the old JWKS; prove overlap works and the old key is later rejected;
  • revoke a user and measure every session/token/cache exposure window;
  • replay an authorization response/code in a different browser session (state/PKCE must reject it);
  • present a workload token to a user endpoint (audience/type mismatch must reject);
  • remove a service relationship and prove cached policy expires;
  • rotate a webhook secret with overlap, then reject the old version.

"Short-lived" is not a number until you've combined token, cache, queue, retry, and offline-processing lifetimes — the real exposure window is the max across all of them.

TLS / mTLS Operations Lab

Capture local handshakes and identify negotiation, certificate chain, key exchange, and the encrypted records (openssl s_client -connect, tshark -O tls). Then test failure handling: hostname mismatch, unknown CA, expiration, missing intermediate, absent client certificate (mTLS), and rotation overlap. Operational rules:

  • automate certificate issuance and rotation; design overlapping rotation and emergency revocation;
  • separate workload identity from DNS routing;
  • validate the trust domain and intended service, not just chain validity (mTLS authenticates the peer; the app still authorizes the action);
  • monitor expiry and failed-handshake rates;
  • never disable verification to troubleshoot — fail closed with useful telemetry instead.

Logging Standard

Record: subject ID, tenant, action, resource ID, policy version, decision, reason, request ID, and latency. Never log: bearer tokens, assertions, recovery URLs, passwords, private keys, or sensitive request bodies. Add canary tokens/secrets and assert via test that they never appear in any log sink.

Chained Failure Analysis

Real breaches chain individually-moderate flaws. Model the chain and break it at each link independently:

weak outbound-URL validation → metadata/workload credential → valid cloud identity
   → broad data permission → incomplete data-access logging

Independent controls: URL policy, egress restriction, credential delivery (no ambient creds), IAM least privilege, data-layer authorization, and detection. Label each control preventive / blast-radius reduction / detective / recovery so a reviewer sees the defense-in-depth, not a single point of failure.

Penetration-Test Report Standard

For each owned-lab finding: request preconditions, the exact security property violated, the minimum proof, affected routes/objects, root cause, the fix, a regression test, a detection, and residual risk. Avoid payload collections with no engineering analysis — a report is findings and fixes, not a list of strings.

Production Proof

  • deployed route inventory matched to authorization tests (no untested route);
  • key/session/secret rotation with measured exposure windows;
  • egress tests including redirects and DNS changes;
  • canaries proving tokens/secrets never enter logs;
  • datastore queries proving the tenant filter is not caller-controlled;
  • incident runbooks for signing-key, session-store, and webhook-secret compromise.

Worked Examples

JWT verification — the vulnerable vs the correct call

The single most common identity bug is decode() masquerading as verify(). Vulnerable:

import jwt
claims = jwt.decode(token, options={"verify_signature": False})   # DISASTER: trusts anything
user = claims["sub"]

An attacker simply forges a token with any sub. Now the correct verification (Phase 02 WARMUP, Chapter 8) — note every pinned parameter:

claims = jwt.decode(
    token,
    key=jwks.get_key(unverified_header["kid"]),  # resolve key by TRUSTED kid from a known JWKS
    algorithms=["RS256"],                         # PIN the algorithm — reject 'none' & HS/RS confusion
    issuer="https://idp.example.com/",            # iss must be our trusted issuer
    audience="https://api.example.com/orders",    # aud — token must be minted FOR THIS service
    leeway=30,                                     # bounded clock skew for exp/nbf
)
# Then still authorize: claims['sub'] may touch THIS object? (object-level authZ, separate step)

The two classic forgeries this blocks: {"alg":"none"} (no signature) and algorithm confusion, where an RS256 verifier is tricked into treating the public key as an HMAC secret by sending {"alg":"HS256"}. Pinning algorithms=["RS256"] defeats both.

Reading a TLS handshake

openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_3 </dev/null

What to look for, and what each proves:

Protocol  : TLSv1.3                ← negotiated version (reject < 1.2)
Cipher    : TLS_AES_256_GCM_SHA384 ← AEAD suite + forward-secret key exchange
Verify return code: 0 (ok)         ← chain validated to a trusted root
   subject=CN=api.example.com       ← name must match the host you asked for (SNI/SAN)
   issuer=CN=Trusted Root CA

A Verify return code: 21 (unable to verify the first certificate) means a missing intermediate — a real production outage cause, not a reason to disable verification. The interview trap: "TLS is green, so the request is authorized." No — TLS authenticated the server's name; the app still authorizes the action (WARMUP Chapter 4).

An IDOR finding, end to end

# Authenticated as user A (tenant 1). Invoice 8123 belongs to tenant 2.
GET /api/invoices/8123      → 200 OK   {"total": 4200, "tenant": 2}   ← BUG: cross-tenant read

Root cause : route authenticates the user but trusts the path object ID without a tenant/
             relationship check.
Fix        : SELECT ... WHERE id = :id AND tenant_id = :caller_tenant   (filter not caller-controlled)
Regression : test_reader_cannot_read_other_tenant_invoice() asserts 404 (not 403 — don't leak existence)
Detection  : alert on object-access where resource.tenant != subject.tenant

Interview Drills

  1. "Walk me through OAuth authorization-code + PKCE on a whiteboard." Draw browser → authz server (with client_id, exact redirect_uri, state, code_challenge) → user authenticates → redirect back with code + state → app verifies state, exchanges code on the back channel with code_verifier. Then explain what each anti-forgery element prevents (state = login CSRF, PKCE = stolen-code reuse, exact redirect URI = code theft, nonce = ID-token replay).
  2. "A user reads another tenant's record by changing a URL ID. Diagnose and fix." IDOR/BOLA: name the root cause (trusting a user-controlled object ID post-auth), fix with object-level authZ on the full tuple, return 404 not 403, add a per-route×tenant test, add a detection. (Worked above.)
  3. "How do you stop SSRF in a webhook fetcher?" Parse once → scheme/port allowlist → resolve via a controlled resolver and validate the resolved IP → re-validate every redirect (pin the address to beat DNS rebinding) → egress proxy → no ambient credentials. Validate the address you connect to, not the string.
  4. "XSS vs CSRF — different fixes, why?" CSRF abuses ambient cookie authority (the attacker causes a request but can't read the response) → SameSite + anti-CSRF token + Origin check. XSS runs script inside your origin with full user authority → contextual output encoding + CSP. HttpOnly blocks cookie theft, not the script making authenticated requests.
  5. "Where else does object-level authZ have to apply besides direct GETs?" Batch, search, export, GraphQL resolvers, background jobs, cache hits, counts, and even error messages — every path that returns data.

References

  • RFC 9700 (OAuth 2.0 Security BCP), RFC 7636 (PKCE), OpenID Connect Core; RFC 7519/7515 (JWT/JWS).
  • RFC 8446 (TLS 1.3), RFC 5280 (X.509); RFC 6265bis (cookies / SameSite).
  • OWASP ASVS; OWASP Cheat Sheets (Authorization, Session, CSRF, SSRF, Deserialization).
  • PortSwigger Web Security Academy labs (OAuth, request smuggling, SSRF).
  • NIST SP 800-63B (authentication); NIST SP 800-57 (key management).