API Security · beginner · ~9 min

API documentation, Swagger, and discovery

**What you will learn** - Locate an API's documentation sources: OpenAPI/Swagger specs, Postman collections, and GraphQL introspection. - Go beyond the docs to find hidden endpoints from client JavaScript, traffic capture, path/parameter fuzzing, and version sprawl. - Turn a raw endpoint list into a structured test map (parameters, objects, expected role) that feeds the BOLA, BFLA, and mass-assignment tests. - Recognise that an exposed production spec or enabled introspection is a finding in its own right, and explain *why*. - Apply a lab-only, authorized workflow with logging, cleanup, and a defensive remediation for each finding.

Overview

Security objective. The asset you are protecting is the complete attack surface of an API — every endpoint, parameter, object type, and privileged function. The threat is an attacker who finds an endpoint your team forgot existed (an old version, an internal admin route, a debug handler) and attacks it because nobody tested it. In this lesson you learn to detect that surface yourself, the way a defender or an authorized tester would, so nothing is left in the dark.

What discovery is. Discovery is the disciplined work of mapping an API completely. An API is opaque from the outside: you see only the responses to requests you already know how to make. Discovery converts that opaque service into a written, testable map.

Where the map comes from. Several sources describe an API and are frequently left reachable:

  • OpenAPI/Swagger specifications (a machine-readable description of every route).
  • Postman collections that were shared or leaked.
  • GraphQL introspection, which dumps the whole schema unless it is turned off.

Documentation rarely covers everything, so the rest of the map comes from reading client-side JavaScript, capturing app traffic, fuzzing paths and parameters, probing old versions, and trying undocumented HTTP methods.

How it connects to the prereq. In The API attack surface you learned to think of an API as a collection of entry points and trust boundaries. Discovery is the practical step that enumerates that surface: it produces the concrete list of entry points that later authorization and input-validation tests iterate over. Without a complete map, every test you run is incomplete by definition.

Why it matters

In authorized professional work — a scoped API penetration test, a bug-bounty engagement within its rules, or an internal security review — you can only test the endpoints you can see. Any route you miss is untested attack surface. If a real attacker finds it first, the consequences land on real users.

Three concrete reasons discovery matters:

  • Coverage is a deliverable. A pentest report that says "we tested authorization" is only credible if you can show which endpoints were tested. Discovery produces that evidence.
  • Old and hidden routes are where bugs hide. Deprecated /v0 and /v1 endpoints, debug handlers, and internal admin routes often skip the security controls added later to the main app. They are disproportionately vulnerable.
  • The exposure is itself a finding. A public production Swagger UI or an enabled GraphQL introspection endpoint is a reportable weakness on its own, because it hands an attacker the full map for free. Documenting that is part of the job.

Doing discovery well early also makes every downstream test faster and more thorough, because the parameters and schemas you collect tell you exactly what to try.

Core concepts

1. Documentation sources

Definition. Machine-readable or shared descriptions of an API's routes, parameters, and data shapes.

Plain explanation. Teams generate docs to help their own developers. Those same docs, if reachable by outsiders, are a gift to an attacker: a labelled map instead of a blind maze.

How it works.

Source Where it lives What it reveals
OpenAPI/Swagger /swagger.json, /openapi.json, /swagger-ui, /v3/api-docs Every documented endpoint, method, parameter, schema
Postman collection Shared workspace link, leaked .postman_collection.json Real example requests, sometimes with tokens baked in
GraphQL introspection The GraphQL endpoint (often /graphql) via an introspection query The entire type system: queries, mutations, fields

When / when-not. Always check for these first in an authorized test — they are the cheapest, highest-value source. Do not assume they are complete; docs describe the intended API, not the actual one.

Pitfall. Reading a spec is not the same as confirming the endpoints exist and behave as described. Always verify by making the request in your lab.

2. Going beyond the docs

Definition. Finding endpoints that no published document lists.

Plain explanation. The app itself is honest about which routes it calls. Its JavaScript, its network traffic, and its error behaviour leak the true endpoint list.

How it works.

  • Client JS and traffic. Read the front-end bundle and capture the requests the app actually makes; they include routes omitted from docs.
  • Fuzzing. Send many candidate paths from a wordlist and watch which return something other than a plain 404.
  • Version sprawl. Swap /v2 for /v1 or /v0; older versions may lack newer checks.
  • Undocumented methods. Try PUT, DELETE, PATCH, OPTIONS on a path you already know from a GET.

When / when-not. Do this only inside your authorization scope. Fuzzing generates load and noise, so respect rate limits and the rules of engagement.

Pitfall. Fuzzing produces false positives: a server that returns 200 for everything, or soft-404 pages, will make dead paths look real. Confirm each hit manually.

3. Turning the map into tests

Definition. Structuring each discovered endpoint so it can drive a specific security test.

Plain explanation. A flat list of URLs is not yet useful. For each endpoint, record what it takes in, what it acts on, and who is supposed to call it.

How it works. For every endpoint note three things: its parameters, the objects it touches (e.g. an order id, a user id), and the role it expects (anonymous, user, admin). That triple is exactly what the BOLA (object-level authorization), BFLA (function-level authorization), and mass-assignment tests consume. The schema is a bonus here: field names in the schema are the candidates you try in mass-assignment.

When / when-not. Do this continuously as you discover, not at the end — the structure guides where to dig next.

Pitfall. Skipping the role column. Without knowing the expected caller, you cannot tell an authorization bug from correct behaviour.

4. Exposure as a finding

Definition. The presence of the discovery surface itself being a vulnerability.

Plain explanation. Docs are fine on an internal developer portal. Reachable from the public internet on production, they lower the cost of every other attack.

How it works. Exposed production Swagger UI, enabled GraphQL introspection, and live deprecated versions each get written up as findings — with severity based on what they expose and how reachable they are, not automatically "critical."

Threat model

                    TRUST BOUNDARY (public internet)
  ATTACKER / TESTER  |                                  |  SERVER SIDE
  (unauthenticated)  |                                  |
                     |                                  |
  browser / curl ----+--> [ /swagger-ui  /openapi.json ]---> full endpoint map
                     |    ENTRY POINT: docs routes      |
                     |                                  |
  fuzzer -----------+--> [ /v1  /v0  /debug  /admin ]---> hidden/old routes
                     |    ENTRY POINT: undocumented     |    (weaker checks)
                     |                                  |
  GraphQL query ----+--> [ /graphql (introspection) ]---> entire schema
                     |    ENTRY POINT: introspection    |
                     |                                  |
  ASSET PROTECTED: the complete, minimal attack surface
  (only intended endpoints reachable; docs + introspection restricted;
   deprecated versions retired)

Knowledge check

  • What asset is protected here? The complete attack surface — the goal is that only intended endpoints are reachable and that the map is not handed to outsiders.
  • Where is the trust boundary? Between the unauthenticated public internet and the server; docs, introspection, and old versions should sit inside it, not be reachable across it.
  • What insecure assumption causes the exposure? "Nobody will find the docs / old version if we don't link to them" — security by obscurity, which discovery defeats.
  • Which logs detect discovery activity? Access logs showing bursts of 404s from one source (fuzzing), hits on /swagger* or /openapi*, and introspection queries on the GraphQL endpoint.
  • Why only in an authorized lab? Fuzzing and enumeration are intrusive; running them on systems you do not own or are not authorized to test is unlawful and unethical.

Syntax notes

The core building blocks are HTTP requests you already know, plus one GraphQL query. All examples target a local lab (localhost).

# Fetch an OpenAPI/Swagger spec (lab host only)
GET /openapi.json HTTP/1.1
Host: localhost:8000
# A 200 with JSON that has an "openapi" or "swagger" key = spec exposed.
# Minimal GraphQL introspection probe: does introspection work at all?
# Send as the "query" field of a POST to the GraphQL endpoint.
query { __schema { queryType { name } } }
# A non-null __schema in the response = introspection is ENABLED (a finding).
# Directory/endpoint fuzzing with ffuf against a LOCAL lab only.
# FUZZ is replaced by each word in the list; -mc 200,401,403 shows
# "exists but maybe protected" responses and hides plain 404s.
ffuf -u http://localhost:8000/FUZZ -w wordlist.txt -mc 200,401,403

Note the interpretation rules: a 401/403 is still a discovery (the route exists, it just needs auth); a 200 on a nonsense path warns of a soft-404 you must rule out.

Lesson

You can't test what you can't see. Discovery turns an opaque API into a complete, testable map.

Documentation sources

  • OpenAPI / Swagger specs (/swagger.json, /openapi.json, /swagger-ui) describe every endpoint, parameter, and schema. They are ideal for testing, and often left publicly exposed.
  • Postman collections that have been shared or leaked.
  • GraphQL introspection (if the API uses GraphQL) dumps the entire schema unless it is disabled.

Beyond the docs

Docs rarely list everything. Also do the following:

  • Read the client JS and mobile app traffic for endpoints the docs omit.
  • Fuzz paths (with a tool such as ffuf) and parameters.
  • Try other versions (/v1 to /v2, or a deprecated /v0), which may have weaker checks.
  • Try undocumented HTTP methods on paths you already know.

Turning the map into tests

For every endpoint you discover, note three things:

  • its parameters
  • the objects it touches
  • the role it expects

Then run the authorization tests (BOLA/BFLA), mass-assignment checks, and rate-limit checks from this track.

The schema is also useful here: it reveals field names to try in mass-assignment.

Defensive note

The following are findings in their own right, because they hand attackers the map:

  • exposed production Swagger
  • enabled GraphQL introspection
  • live deprecated versions

Code examples

The workflow below is INSECURE server → SECURE server → VERIFY. It runs entirely on localhost against a tiny lab API you control. Nothing here targets a third party.

Authorization checklist (before running anything):

  • The target is a service you own, running on your own machine (localhost) or an isolated container.
  • No production data or real credentials are loaded.
  • You have written scope; for this lesson the scope is "my local lab only."
  • You know the cleanup/reset steps (below) before you start.

1. WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.

This lab API leaves its OpenAPI spec public and keeps a deprecated /v0 route with no auth.

# vuln_api.py  -- LAB ONLY. Do not deploy.
from flask import Flask, jsonify, request

app = Flask(__name__)

# INSECURE: spec served publicly in production-like mode
@app.get("/openapi.json")
def spec():
    return jsonify({
        "openapi": "3.0.0",
        "paths": {
            "/v1/orders/{id}": {"get": {}},
            "/v0/orders/{id}": {"get": {}},   # deprecated, still live
        },
    })

# INSECURE: deprecated v0 with NO auth check
@app.get("/v0/orders/<int:id>")
def v0_order(id):
    return jsonify({"id": id, "total": 42, "note": "legacy, no auth"})

# The intended, current route DOES check a token
@app.get("/v1/orders/<int:id>")
def v1_order(id):
    if request.headers.get("Authorization") != "Bearer lab-token":
        return jsonify({"error": "unauthorized"}), 401
    return jsonify({"id": id, "total": 42})

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8000)

A tester fetches /openapi.json, sees both /v1 and /v0, and finds that /v0/orders/1 returns data with no token. The exposed spec plus the live deprecated route are the findings.

2. SECURE fix

Restrict the spec to non-production, retire the deprecated route, and (if the older behaviour must stay) enforce the same auth on every version.

# secure_api.py  -- LAB ONLY illustration of the fix.
import os
from flask import Flask, jsonify, request

app = Flask(__name__)
IS_PROD = os.environ.get("ENV") == "production"

def require_token():
    return request.headers.get("Authorization") == "Bearer lab-token"

# FIX 1: only serve the spec outside production
@app.get("/openapi.json")
def spec():
    if IS_PROD:
        return jsonify({"error": "not found"}), 404
    return jsonify({"openapi": "3.0.0", "paths": {"/v1/orders/{id}": {"get": {}}}})

# FIX 2: deprecated route retired -> 410 Gone, and still auth-gated
@app.get("/v0/orders/<int:id>")
def v0_order(id):
    return jsonify({"error": "gone; use /v1"}), 410

# FIX 3: current route enforces auth (unchanged, correct)
@app.get("/v1/orders/<int:id>")
def v1_order(id):
    if not require_token():
        return jsonify({"error": "unauthorized"}), 401
    return jsonify({"id": id, "total": 42})

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8000)

3. VERIFY — prove the fix rejects bad input and accepts good input

# Run the secure server in production mode for the test:
ENV=production python secure_api.py &

# (a) Spec must be hidden in production  -> expect 404
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/openapi.json
# expected: 404

# (b) Deprecated route must be gone      -> expect 410
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/v0/orders/1
# expected: 410

# (c) Current route without a token       -> expect 401 (rejects bad input)
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/v1/orders/1
# expected: 401

# (d) Current route WITH the token        -> expect 200 (accepts good input)
curl -s -o /dev/null -w "%{http_code}\n" \
     -H "Authorization: Bearer lab-token" http://127.0.0.1:8000/v1/orders/1
# expected: 200

Expected output: 404, then 410, then 401, then 200. Together these show the spec is no longer exposed, the old route is retired, and legitimate authorized access still works.

Cleanup / reset: stop the background server (kill %1 or Ctrl-C), unset ENV (unset ENV), and delete any wordlists or captured output you created for the exercise.

Line by line

Walkthrough of the vulnerable-to-verified flow.

Step Action What happens Why it matters
1 GET /openapi.json on the vulnerable server Returns JSON listing /v1/orders/{id} and /v0/orders/{id} The spec hands over the whole map, including a route not meant to be advertised
2 Read the map Two versions of the same resource are visible Version sprawl spotted — /v0 is the interesting one
3 GET /v0/orders/1 (no token) 200 with order data Deprecated route skips the auth the current route enforces — a real vulnerability
4 GET /v1/orders/1 (no token) 401 Confirms the intended control exists, isolating /v0 as the gap
5 Apply fixes Spec 404s in prod, /v0 returns 410, /v1 unchanged Surface is minimised and every remaining route is auth-gated
6a Verify spec curl404 Map is no longer free to outsiders in production
6b Verify old route curl410 Gone Deprecated surface removed; 410 also tells honest clients to move on
6c Verify current route, no token curl401 The fix rejects unauthenticated access (bad input)
6d Verify current route, with token curl200 The fix still accepts legitimate access (good input) — no over-correction

The key value change is the response code on the deprecated route: 200 (leaking data) before, 410 (retired) after, while the current route's 401/200 split proves auth is enforced without breaking real users.

Common mistakes

Mistake 1 — Trusting the spec as the whole truth.

  • Wrong: Testing only the endpoints listed in /openapi.json and calling coverage complete.
  • Why wrong: Docs describe the intended API. Debug routes, old versions, and internal handlers are usually undocumented — and are where the weak checks live.
  • Corrected: Treat the spec as a starting point; add client-JS reading, traffic capture, and scoped fuzzing.
  • Recognise/prevent: Keep a checklist with a "beyond the docs" section that must be filled in before you sign off.

Mistake 2 — Counting soft-404s as real endpoints.

  • Wrong: Reporting every 200 from a fuzzer as a live route.
  • Why wrong: Some servers return 200 (or a friendly HTML page) for missing paths, so nonsense paths look real.
  • Corrected: Baseline with a deliberately random path first; compare response length and body, and confirm each hit manually.
  • Recognise/prevent: If a garbage path like /zzz-not-real-9182 returns 200, distrust all 200s until you filter by size/content.

Mistake 3 — Confusing 'documented' with 'exists' (and 'decoded' with 'verified').

  • Wrong: Assuming a spec entry is real, or assuming that reading a JWT from a response proves it is valid.
  • Why wrong: A spec can be stale, and decoding a token only reveals its claims — it does not verify the signature or that the route is live.
  • Corrected: Confirm existence by making the request in the lab; never equate reading data with validating it.
  • Recognise/prevent: Separate your notes into "documented" vs "confirmed reachable."

Mistake 4 — Fuzzing out of scope or ignoring rate limits.

  • Wrong: Pointing a fuzzer at a domain you were not authorized to test, or hammering it.
  • Why wrong: It is unlawful, and it can degrade the service (effectively a denial of service).
  • Corrected: Fuzz only in-scope hosts, throttle to the rules of engagement, and log what you send.
  • Recognise/prevent: Keep the scope document open; if a host is not on it, do not touch it.

Debugging tips

Common problems and how to work through them.

  • The spec URL 404s. Try the common variants: /openapi.json, /swagger.json, /swagger-ui, /v3/api-docs, /api-docs. Also check the HTML of the app for a link to the docs, and the JS bundle for a basePath.
  • Introspection returns an error. A 403/validation error on the introspection query usually means introspection is disabled — note that as good hygiene, not a failure. Confirm you are POSTing to the right GraphQL endpoint and sending the query in the query field.
  • Fuzzer shows everything as found. You have a soft-404. Add a filter (in ffuf, -fc 404 won't help here; instead filter by response size with -fs <size> after baselining a known-bad path).
  • curl returns nothing / connection refused. The lab server is not running or is on a different port. Check it is bound to 127.0.0.1:8000 and started.
  • A discovered route returns 401/403. That is still a discovery — the route exists and is auth-gated. Record it as reachable-but-protected; do not discard it.

Questions to ask when discovery stalls:

  • What does a known-bad path return? (Establish your 404 baseline first.)
  • Am I looking at the front-end's real traffic, or only the docs?
  • Are there version prefixes I have not tried (/v0, /v1, /beta, /internal)?
  • Have I tried other HTTP methods on routes I already found?
  • Is anything I am hitting outside my authorized scope?

Memory safety

Security & safety — detection and logging.

Discovery is noisy by nature, which is good news for defenders: it is very detectable. Whether you are the tester documenting your own activity or the defender building alerting, log the right things.

What to log for each request/decision:

  • Timestamp (with timezone).
  • Source identifier (source IP, and authenticated principal / API key id if present).
  • Resource requested (method + path, and whether it was a docs/introspection route).
  • Result (HTTP status, and response size to help spot soft-404 scanning).
  • Security decision (allowed / denied, and which control fired — auth, rate limit).
  • Correlation id so one client's burst of requests can be reconstructed.

What to NEVER log:

  • Passwords, API tokens, session cookies, or Authorization header values.
  • Private keys or full secrets.
  • Full payment card numbers (PANs) or unneeded PII.
  • Full request bodies that may contain the above — redact first.

Events that signal abuse (or an active discovery attempt):

  • A spike of 404s from one source in a short window (path fuzzing).
  • Repeated hits on /swagger*, /openapi*, /api-docs, or __schema introspection queries.
  • Traffic to deprecated version prefixes (/v0) that legitimate clients no longer use.
  • Unusual HTTP methods (OPTIONS, PUT, DELETE) probed across many paths.

How false positives arise:

  • A misconfigured client or a search-engine crawler can generate many 404s without malice.
  • Health checks and uptime monitors hit odd paths on a schedule.
  • A legitimate integration may still call a deprecated route until it is migrated.

Tune alerts on rate and diversity of paths per source, and correlate with authentication state, so a single mistyped URL does not page anyone while a real fuzzing sweep does.

Real-world uses

Authorized real-world use case. A company hires a firm for a scoped API penetration test. On day one the testers pull the staging OpenAPI spec, capture the mobile app's traffic, and run throttled path fuzzing within the rules of engagement. The output is a spreadsheet of every endpoint with its parameters, objects, and expected role — the map that the rest of the engagement (authorization, mass-assignment, and rate-limit testing) works through. Two findings come straight from discovery: a production Swagger UI reachable without auth, and a live /v0 that skips the newer authorization checks.

Best-practice habits.

Habit Beginner Advanced
Validation Confirm each discovered route by requesting it in the lab Baseline soft-404s and filter fuzzer output by size/content
Least privilege Note the expected role for every endpoint Cross-check that each version enforces the same role as the current one
Secure defaults Keep specs out of production; disable introspection there Serve docs only behind auth on an internal network; gate by environment flag
Logging Record every request you send during testing Build detections for docs hits, introspection, and 404 bursts
Error handling Treat 401/403 as "exists but protected," not "nothing" Retire deprecated routes with 410 Gone and monitor residual traffic

Defensive framing. For each discovery finding, the deliverable is remediation plus verification: restrict or remove the exposure, then re-test to confirm the map is no longer free and legitimate access still works. Never claim the API is "completely secure" — claim that the tested surface behaved correctly on retest.

Practice tasks

All tasks are lab-only: run them against a service you own on localhost or in an isolated container. Do not point any tool at a system you are not authorized to test.

Beginner 1 — Find the spec.

  • Objective: Locate a lab API's OpenAPI/Swagger spec.
  • Requirements: Try at least four common paths (/openapi.json, /swagger.json, /swagger-ui, /v3/api-docs).
  • Input/Output: Input = requests to your lab host; output = the path that returns a spec and the count of endpoints it lists.
  • Constraints: localhost only.
  • Hints: A JSON body with an openapi or swagger key confirms it.
  • Concepts: documentation sources, verification.
  • Defensive conclusion: If the spec is reachable in production mode, note it as a finding and gate it behind an environment flag, then re-request to confirm it now 404s.

Beginner 2 — Build the map triple.

  • Objective: For every endpoint in the spec, record parameters, objects touched, and expected role.
  • Requirements: Produce a small table.
  • Input/Output: Input = the spec from task 1; output = a table with three columns filled per endpoint.
  • Constraints: No requests needed beyond confirming existence.
  • Hints: The parameters and requestBody schema in the spec give you the fields.
  • Concepts: turning the map into tests.
  • Defensive conclusion: Flag any endpoint whose expected role you cannot determine — undefined roles are a design smell to report.

Intermediate 1 — Scoped path fuzzing with a baseline.

  • Objective: Discover an undocumented route by fuzzing, without being fooled by soft-404s.
  • Requirements: First request a deliberately random path to baseline the 404 behaviour, then run a small wordlist with ffuf against your lab.
  • Input/Output: Input = wordlist + lab host; output = the real route(s), with the soft-404 hits filtered out.
  • Constraints: Throttle requests; localhost only.
  • Hints: Compare response size against your baseline; treat 401/403 as valid discoveries.
  • Concepts: fuzzing, false positives.
  • Defensive conclusion: For any undocumented route found, propose whether it should be removed, auth-gated, or documented, then verify the chosen fix.

Intermediate 2 — GraphQL introspection check.

  • Objective: Determine whether a lab GraphQL endpoint has introspection enabled.
  • Requirements: Send the minimal { __schema { queryType { name } } } probe.
  • Input/Output: Input = POST to the GraphQL endpoint; output = enabled/disabled conclusion with the evidence.
  • Constraints: localhost only.
  • Hints: A non-null __schema means enabled; an error usually means disabled.
  • Concepts: introspection, exposure as a finding.
  • Defensive conclusion: If enabled on a production-like config, disable introspection there and re-run the probe to confirm it now errors.

Challenge — Full discovery report on the lab.

  • Objective: Produce a mini finding for each discovery-level issue in your lab (exposed spec, live deprecated version, enabled introspection — whichever apply).
  • Requirements: For each, write title, severity (justified by exposure and reachability, not automatically critical), affected component, safe reproduction, impact, remediation, and a retest step.
  • Input/Output: Input = all evidence from earlier tasks; output = a short structured report.
  • Constraints: No exploitation beyond confirming existence; localhost only; redact any tokens.
  • Hints: Severity for an exposed staging spec differs from one exposed on production — reachability drives it.
  • Concepts: exposure as a finding, mitigation verification, reporting discipline.
  • Defensive conclusion: Each finding must end with a remediation and a retest that proves the map is no longer free while legitimate access still works. Then run the cleanup/reset steps.

Summary

  • Discovery turns an opaque API into a complete, testable map — you can only test what you can see, so missed endpoints are untested attack surface.
  • Start with documentation sources: OpenAPI/Swagger (/openapi.json, /swagger-ui), Postman collections, and GraphQL introspection ({ __schema { queryType { name } } }).
  • Then go beyond the docs: client JS and traffic, scoped ffuf fuzzing, version sprawl (/v0, /v1), and undocumented HTTP methods. Baseline soft-404s so dead paths do not masquerade as real ones.
  • Structure the map as (parameters, objects, expected role) per endpoint; that triple feeds the BOLA, BFLA, and mass-assignment tests, and schema field names feed mass-assignment.
  • Common mistakes: trusting the spec as complete, counting soft-404s, confusing documented with reachable (and decoded with verified), and fuzzing out of scope.
  • Exposure is itself a finding: public production Swagger, enabled introspection, and live deprecated versions. Rate severity by exposure and reachability, not by default.
  • Defensive core: gate docs behind environment/auth, retire old routes with 410, disable introspection in production, log discovery-shaped traffic (never log secrets), and finish every finding with a remediation and a retest. Run on authorized labs only, and clean up afterwards.
  • Remember: decoding a token is not verifying it, passing a scanner is not proof of security, and nothing is ever "completely secure."

Practice with these exercises