~/learn / apis / rest

REST APIs

How REST models data as resources, the conventions for URLs, pagination, filtering, caching, and errors, the maturity levels real APIs sit at, and the design choices that create security bugs.

beginnerupdated

REST is less a protocol than a set of conventions for using HTTP well. Once you know the conventions you can predict endpoints you have never seen, which is exactly what makes REST APIs so explorable during testing. The conventions are a map, and a map works for the attacker too.

Resources and URLs

REST treats everything as a resource with a URL. Collections are plural nouns; individual items are addressed by id:

GET    /users              list users
POST   /users              create a user
GET    /users/42           read user 42
PATCH  /users/42           update part of user 42
DELETE /users/42           delete user 42
GET    /users/42/orders    orders belonging to user 42

When you see /users/42/orders, the conventions tell you to try /users/43/orders, /orders, and /users/42/orders/7. Predictability is a feature for developers and a map for testers, and the nested form (/users/42/orders) is where object-level authorization is most often forgotten, because the developer “already checked the user.”

Representations

The server returns a representation of the resource, almost always JSON today:

{
  "id": 42,
  "name": "Lab User",
  "email": "[email protected]",
  "role": "user",
  "createdAt": "2026-03-14T09:26:53Z"
}

A frequent mistake is returning the whole database row when the client needs three fields. Responses that include passwordHash, resetToken, internal flags, or other users’ data are excessive data exposure, trivial to spot in a proxy even when the UI hides them. The fix is an explicit output schema (a serializer or DTO) that lists what may leave, rather than serializing the model directly.

Querying collections

GET /products?category=books&sort=-price&limit=20&cursor=eyJpZCI6MTAwfQ
Concern Common patterns
Filtering ?status=open, ?createdAfter=2026-01-01
Sorting ?sort=price, ?sort=-createdAt for descending
Pagination ?page=3&size=20, or cursor-based ?cursor=...
Field selection ?fields=id,name

Every one of these is attacker-controlled input. A large limit can exhaust memory or the database, a filter on an unexpected field can leak data the UI never requests, and sort parameters have a long history of being concatenated straight into SQL ORDER BY, which cannot be parameterized and so needs an allowlist (see SQL injection). Cursor pagination is both more efficient than offset pagination and harder to abuse, since the cursor encodes a position rather than an arbitrary skip count.

Caching and conditional requests

REST leans on HTTP caching, and the mechanics are worth knowing because they affect both performance and security. A server returns an ETag (a version fingerprint) and Cache-Control. The client can later send If-None-Match: <etag>, and the server answers 304 Not Modified with no body if nothing changed, saving bandwidth. The security angle: a shared cache keyed only on the URL can serve one user’s personalized response to another (web cache deception and cache poisoning). Mark private responses Cache-Control: private, no-store, and never let a cache key omit the authorization context.

The maturity levels real APIs sit at

The Richardson Maturity Model is a useful lens: level 0 is a single endpoint doing everything (RPC in disguise), level 1 introduces resources, level 2 uses HTTP methods and status codes correctly (where most good APIs live), and level 3 adds hypermedia (HATEOAS), where responses carry links to the next available actions. Most production APIs are level 2, and knowing this tells you to expect proper methods and status codes and to treat deviations as either intentional design or a bug worth probing.

Errors

Good APIs return a consistent error format with the right status code, such as RFC 9457 problem details:

{
  "type": "https://api.example.com/problems/insufficient-funds",
  "title": "Insufficient funds",
  "status": 422,
  "detail": "Balance is 10.00, but the order total is 39.98."
}

Errors that leak stack traces, SQL fragments, internal hostnames, or framework versions hand an attacker a free reconnaissance report, which is why production errors should be generic while the detail goes to logs.

Versioning and forgotten inventory

APIs evolve, so they are versioned: /v1/users, /v2/users, or a header like Accept: application/vnd.example.v2+json. Old versions are frequently left running with weaker controls, because the fix went into the new version and nobody retired the old one. If the app calls /v3/, try /v2/ and /v1/. Forgotten versions and undocumented hosts are a staple finding, improper inventory management in the OWASP API Top 10.

Documentation is recon

Many REST APIs publish an OpenAPI (formerly Swagger) description: a machine-readable list of every endpoint, parameter, and schema. Look for it at /openapi.json, /swagger.json, /v3/api-docs, or /docs. Import it into Postman or Burp and you have the full map instantly, including endpoints the UI never calls, which are exactly the ones most likely to be under-protected.

Field notes: mapping an API you were not given docs for

The UI calls a fraction of the real API. You find the rest. First, proxy the app through Burp and just use it, which populates the sitemap with every real call. Then expand from there:

  • Grep the JavaScript bundles. Modern SPAs hardcode every endpoint, and often keys, in their JS. Pull them and extract paths: curl -s https://app.test/main.js | grep -oE '"/api/[^"]+"'. Tools like LinkFinder and katana do this at scale.
  • Hunt for the spec. Try /openapi.json, /swagger.json, /v3/api-docs, /swagger-ui, /api-docs, /graphql (introspection), /.well-known/. One hit gives you the entire map. Import it into Postman or Burp.
  • Brute-force routes with a content-aware fuzzer when there is no spec. kiterunner is purpose-built for APIs (it knows REST path patterns and methods, unlike a plain directory buster): kr scan https://api.test -w routes.kite -A=apiroutes.
  • Roll back the version. If the app uses /v3/, request /v2/ and /v1/. Old versions survive with weaker or missing controls constantly.

Field notes: the bugs that live in REST conventions

  • IDOR by incrementing. /users/42/orders invites /users/43/orders. The nested form is where object-level checks are forgotten. See the two-account method.
  • Excessive data exposure. The mobile app shows three fields; the JSON returns forty, including passwordHash, ssn, internal flags, and other users’ records. Always read the raw response in a proxy, never the rendered screen. The UI filtering is cosmetic.
  • Method tampering. A 403 on DELETE may be 200 on POST with _method=DELETE, or on an undocumented PATCH. Controls bind to verbs unevenly.
  • Mass assignment. PATCH /users/42 with {"role":"admin"} or {"account_balance":99999} when the server blindly binds the body to the model. Add fields the UI never sends.
  • ORDER BY and filter injection. ?sort= and ?filter= reach SQL in ways that cannot be parameterized, needing an allowlist. Probe them for SQL injection.
  • Cache deception. Request /account/profile/nonexistent.css; if a shared cache stores the authenticated response under that key, the next visitor to the URL gets your data.

Takeaway

REST’s consistency is what makes it pleasant to build with and productive to test. Learn the conventions, then look for the places an API breaks them, because that is usually where someone forgot a check.