Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 4 min read

Treat Every ID as an Authorization Claim

Authentication answers one important question: who made this request? It does not answer a second question that often hides inside an ordinary-looking query parameter: may this caller name the record represented by this

Authentication answers one important question: who made this request? It does not answer a second question that often hides inside an ordinary-looking query parameter: may this caller name the record represented by this identifier?

That gap is easy to miss in APIs that accept a parent ID, child ID, account ID, project ID, or tenant ID. The route is authenticated. The identifier has the correct shape. The query returns a valid result. Yet none of those facts proves that the record belongs inside the caller's authorized scope.

A useful engineering rule is to treat every caller-supplied identifier as an authorization claim before treating it as a query input.

Authentication Is Not Object Ownership

Imagine an endpoint that returns activities for a supplied household record. The controller requires a signed-in user, then passes the supplied ID into a query service. If the ID is changed in transit, the query may still succeedβ€”just for somebody else's data.

The vulnerable assumption is subtle: β€œsigned in” becomes β€œallowed to ask about any existing record.” The database faithfully answers the question it receives. The missing control belongs before the query.

The server should derive the caller's authority from trusted identity and relationship data. For an ordinary user, that might mean resolving the account to its authoritative person record, then traversing explicit relationships to the permitted records. For an operational role, a specific permission may allow a wider scope because that role already performs work requiring the same visibility.

This also explains why email equality is a poor ownership rule. Addresses are corrected, reused, shared by families, copied into contact fields, and sometimes retained after account changes. An email can help locate a candidate during migration, but it should not silently replace an authoritative account-to-person relationship.

Put the Boundary Before the Query

A safe request pipeline makes the order explicit:

  1. Identify the caller from trusted authentication state.
  2. Resolve the caller's authoritative records and relationships on the server.
  3. Evaluate each supplied identifier against that scope.
  4. Only then construct and execute the data query.

This order prevents a common failure mode: retrieving a broad result and hoping a later projection removes everything sensitive. Late filtering spreads the security contract across repositories, mappers, and UI code. Early scope resolution gives the query a smaller, already-authorized input.

Do not bundle independent identifiers into one vague decision. If a request contains both a parent filter and a child filter, check each one. Mixed requests are exactly where β€œone valid field makes the request valid” bugs appear. A valid parent must not bless an unrelated child; an unrelated parent must not erase a valid child's boundary.

Reject by Default, Narrow Only Deliberately

For most data-specific routes, a cross-scope identifier should be rejected. Rejection is direct, observable, and easier to reason about. It tells the client that its request crossed a boundary and gives operators a signal worth investigating.

Legacy mobile clients complicate that clean answer. An installed client may send an old identifier shape, interpret a forbidden response as a session failure, or sign the user out. Waiting for every old version to disappear can leave a real data boundary open longer than necessary.

There is a narrow alternative: compatibility-preserving narrowing. The server validates every supplied identifier, discards any that are not authorized, and executes only a known-safe baseline query. The key invariant is monotonicity: removing an unauthorized filter must never widen the response into the requested protected data.

For example, a calendar route might combine household-specific entries with public entries. If an old client sends an account ID where a domain record ID is expected, the server can remove that invalid household filter and return only the public baseline. It must not substitute another household, guess from a weak attribute, or run an unscoped β€œall entries” query.

This is not a general recommendation to hide authorization failures. It is an endpoint-specific migration tool. Document why it exists, record how it is used, and give it a removal condition.

Test the Boundary, Not Just the Happy Path

Authorization tests should form a matrix around relationships and permissions:

  • the caller names their own authoritative record;
  • the caller names a related child record;
  • the caller names another household's record;
  • the caller has no corresponding relationship;
  • a shared or copied email appears on a record the caller does not own;
  • a staff role lacks the exact cross-scope permission;
  • a staff role holds that permission;
  • one filter is authorized while another is not;
  • the legacy request shape still receives the same safe baseline.

The last two cases are especially valuable. Mixed filters catch accidental β€œany match” logic. Legacy-shape tests prevent a future cleanup from turning a deliberate compatibility response back into either a leak or a destructive client error.

Tests should assert returned data as well as status codes. A successful response is safe only if its contents stay inside the proven scope. A forbidden response is useful only if the client contract can tolerate it.

Make the Exception Expensive to Forget

Compatibility code tends to become permanent when its rationale lives only in memory. Keep the exception close to the boundary, name the safe baseline, add telemetry that contains no sensitive identifiers, and define the client version or date after which strict rejection can replace narrowing.

The trade-off is real. Strict rejection gives a simpler model and clearer misuse signals. Narrowing adds policy, tests, and the possibility of masking a broken client. Its value is speed: the server can close the data boundary now while preserving a safe experience for older installations.

The practical standard is modest but powerful: never let a caller-provided ID reach a query merely because it parses. First prove that the caller may name it. If compatibility forces an exception, make the request narrower, make the safe response explicit, and make the exception temporary.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.