Open Impersonation Path
ID |
open_impersonation_path |
Severity |
critical (the switch also widens the caller’s authorities) / high |
Remediation Complexity |
medium |
Remediation Risk |
high |
Remediation Effort |
medium |
Family |
API1:2023 - Broken Object Level Authorization |
CWE |
CWE-639, CWE-282, CWE-266 |
Resource |
source |
Language |
Java, Kotlin, C#, Python, JS/TS, PHP, Go, Swift, Ruby |
Description
A request arrives, the application authenticates it, and a few lines later the request is somebody else — an account, a tenant or an organisation the caller named in a header, a parameter or a body field. Impersonation is a feature. Support consoles, "view as user" tools and tenant-switching back offices all need it, and the switch is never the finding.
The finding is the switch whose target nobody checked.
This detector reads the identity census and separates its sites into two kinds:
| Relation | Reading | Whose question |
|---|---|---|
No identity site precedes it |
The site is where this application decides who is calling |
|
An identity site precedes it on every path in |
The site replaces a caller that was already decided |
This detector |
That distinction is what keeps ordinary multi-tenant routing out of the report. Resolving a tenant before a principal exists is how every multi-tenant application routes a request. Changing the tenant of a principal that already exists is a different event, with the same code.
A replacement is reported when both hold:
-
The caller names the target. The value written into the principal, or the value that selects the branch performing the write, reaches the site from a request header, parameter, cookie, path segment or body field. A target that arrives from configuration, from the data store or from another service is not reported: that is how a legitimate switch names who to become.
-
Nothing establishes entitlement over that target. No call on the path in belongs to the
entitlementvocabulary and mentions what the site writes.
Why a role check is not enough
That a caller holds a support role establishes that they may impersonate someone. It does not establish that they may impersonate this someone. Where the only guard names a role and never the target, the finding is reported and says exactly that — the difference between a support console and an open one is that second question.
Why it matters more than its volume suggests
The switch usually happens in a filter, an interceptor or a middleware, outside any handler, so the authorization detectors never look at it. Everything downstream is then correct by construction: every ownership check passes, because the principal being checked is already the impersonated one. One unguarded switch settles every object-level check in the application at once.
Vulnerable example
public class SupportFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, UserDetails caller) {
Authentication auth = jwtProvider.authenticationFrom(request.getHeader("Authorization"));
SecurityContextHolder.getContext().setAuthentication(auth); (1)
if (caller.hasRole("SUPPORT")) { (2)
impersonate(Long.valueOf(request.getParameter("act_as"))); (3)
}
}
}
| 1 | The request is authenticated here. From this line on, it has a caller. |
| 2 | A role check. It establishes that this caller may impersonate someone. |
| 3 | The caller names who. Nothing established that this target is one they may act as, so any holder of the support role is every account in the system. |
Fixed example
public class SupportFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, UserDetails caller) {
Authentication auth = jwtProvider.authenticationFrom(request.getHeader("Authorization"));
SecurityContextHolder.getContext().setAuthentication(auth);
Long target = Long.valueOf(request.getParameter("act_as"));
if (supportService.checkAccess(target, caller)) { (1)
impersonate(target);
auditLog.impersonation(caller, target); (2)
}
}
}
| 1 | The guard names the target, so it answers the question the rule asks: may this caller act as this account. |
| 2 | A support feature nobody can audit is indistinguishable from an open one after the fact. |
Configuration
This detector takes no vocabulary of its own. It reads the identity census taken by
hidden_authentication_path, and the names it tests against live in that detector’s catalog:
-
identityShapes.<language>.entitlement— calls that establish entitlement over something they name. Add this application’s own access check here. -
identityShapes.<language>.requestInput— the reads that make a value caller-controlled.
With hidden_authentication_path disabled there is no census, and this detector reports nothing and
records why.