A security review of Salus brought me back to a question that remains after authentication:
Can this user access this specific resource?
Consider this scenario:
- User A creates patient X.
- User B signs in and receives a valid JWT.
- B calls
GET /patients/X.
If the application looks up the patient by patientId alone, B may receive a patient belonging to A. B is authenticated, but is not authorized to access that object. This is Broken Object Level Authorization (BOLA).
Authorization must reach the query
A valid JWT establishes who made the request. The access rule still needs to use that identity to scope the resource lookup:
patientId + currentUser.id → authorized resource
Instead of:
findById(patientId)
the operation could conceptually use:
findByIdAndOwner(patientId, currentUser.id)
The method name is incidental. The access boundary belongs in the rule and the lookup, including reads, updates, and deletes.
What should the API return for someone else's patient?
A 403 Forbidden response can tell B that patient X exists. When even that fact is sensitive, 404 Not Found may be preferable: X is outside the set of resources B is allowed to see.
The choice depends on the API's policy and should be applied consistently. It makes HTTP responses less useful for probing which IDs exist.
Security as testable behavior
The path does not end at login → JWT → middleware. It continues through the operation on each object:
identity → authentication → authorization → ownership → resource access
This is the property that became a Salus security regression test:
User A creates patient X.
User B tries to access X.
Expected result: 404.
The hardening work reproduced this scenario as a failing test (RED): User A creates X, User B receives a valid JWT and tries to access it. Ownership was then introduced, operations were scoped by ownerId, and cross-user access began returning 404 in this context. The regression test now passes (GREEN), alongside JWT verification on patient routes.
TDD and security regression tests protect this behavior as the system evolves.
Security should be verifiable behavior, not just configuration.
Project: Salus on GitHub
Top comments (1)
Good writeup on BOLA. On the 403 vs 404 choice: worth timing both branches once ownership scoping lands, not just checking the response body -- if the forbidden path short-circuits before the DB lookup while the not-found path still hits the DB, the two responses take different time even with the same status code, and that gap leaks the same fact you're trying to hide. Also curious if Salus's regression suite covers list endpoints -- BOLA on /patients?filter=X can leak existence through which rows come back, even when every single-GET is locked down.