An access log shows v1 order reads across owners while v2 is protected — find and fix the flaw
A partner integration still calls the deprecated v1 order API. The gateway route table and an access-log excerpt from one authenticated session are below.
Constraints:
v1andv2are handled by the same order service, which loads an order by id alone- the gateway rule named
owner-checkcompares the token subject to the order owner - orders 50113, 50114 and 50115 belong to three different customers
- the partner contract does not allow breaking
v2behaviour
gateway routes:
GET /api/v2/orders/{id} -> order-service filters: auth, owner-check
GET /api/v1/orders/{id} -> order-service filters: auth
access log (token subject = customer 8842):
09:12:04 GET /api/v2/orders/50113 403
09:12:19 GET /api/v1/orders/50113 200 1.4kB
09:12:22 GET /api/v1/orders/50114 200 1.5kB
09:12:25 GET /api/v1/orders/50115 200 1.4kB
Find and fix the vulnerability.
The ownership check exists only as a gateway filter on v2, so the deprecated v1 route reaches the same service and returns any order by id. Move the object check into the order service, where every version passes, and retire v1.
- ✗Treating a gateway filter as the authorization control for every route behind it
- ✗Forgetting that a deprecated version still reaches the current service
- ✗Fixing the identifier format or the status code instead of the missing check
- →How would you find every route still reachable but absent from the current specification?
- →What alert on this log would have surfaced the pattern on the first day?
The owner-check filter is attached only to the v2 rule, while v1 reaches the same service through auth alone. The service loads an order by id, so authenticated customer 8842 reads orders belonging to three different owners — broken object-level authorization, with the deprecated version simply routing around the one place the check lived.
The correct fix moves the ownership check into the order service, so every version and every new route passes it, and then removes v1 from the route table. Duplicating the gateway filter is not enough: the next version or an internal caller would bypass it again.
async function getOrderForCaller(orderId, callerId) {
const order = await orders.findByIdForOwner(orderId, callerId);
if (!order) throw new NotFound(); // a foreign order looks like a missing one
return order;
}
Separately, alert on a run of 200s across distinct owners within one session, and reconcile the route table against the current specification so forgotten versions do not survive for years.