ASVS 5.0 V8.3.3 requires that access to an object is based on the permissions of the originating subject, not of an intermediary or service acting for them. It is a Level 3 requirement. If service A calls service B for a user, B should decide using the user's token, not a machine to machine token that A holds.
Why it matters
When a backend calls another service with its own powerful credential, the downstream service sees only "service A". Any bug in A that lets a user ask for someone else's record becomes a data breach, because B will happily answer. This is the confused deputy problem.
How to implement it
Pass the user's identity forward and let each service decide.
- Forward the user's access token, or exchange it for a narrower downstream token using OAuth token exchange (RFC 8693), so the audience is the next service and the subject is still the user.
- Do not give the intermediary a blanket service credential that bypasses per user checks.
// service A calling service B on behalf of the user
const res = await fetch("https://b.internal/records/" + id, {
headers: { Authorization: "Bearer " + userTokenForB },
});
Where a service credential is unavoidable, such as a batch job, pass the user context as signed data and enforce it in B.
How to test it
As user 1, call A with the ID of a record owned by user 2. Expect B to deny it. Then check B's logs show user 1 as the subject. If they show only the service account, you have the gap.
Common mistakes
- One shared machine token used for every downstream call.
- Forwarding a user ID in a plain header that B trusts without verification.
- Forwarding a token whose audience is A to B.
Quick check
Why not simply check permission in service A?
Because B is the one holding the data. Each layer should enforce, so a bug in one does not expose everything.