CVE-2026-66422
Patch Available.
This Vulnerability has been fixed in the Never-Ending Support (NES) version offered by HeroDevs.
Overview
Apache Tomcat is an open-source web server and servlet container, maintained by the Apache Software Foundation, that implements the Jakarta Servlet, Jakarta Pages (JSP), Jakarta Expression Language, and Jakarta WebSocket specifications and provides a pure-Java HTTP environment for running Java web applications.
An authorization bypass vulnerability (CVE-2026-66422) has been identified in the declarative access control of Apache Tomcat, which allows an authenticated user who does not hold the role a resource is reserved for to reach that resource anyway. The Servlet specification lets a servlet declare a private role alias, a <security-role-ref> that maps a name the servlet uses internally onto a real application role, and it confines that alias to the servlet's own HttpServletRequest.isUserInRole() calls. Tomcat also applied the alias while the container was evaluating a <security-constraint>, so when a servlet's alias name collided with a role named in an <auth-constraint>, the role the constraint enforced was replaced by the servlet's linked role.
The replacement is total rather than additive, so the access decision flips in both directions: a user who holds only the linked role is admitted to the protected resource, and a user who genuinely holds the role the constraint names is refused. Readers assessing their own exposure should not assume the effect is limited to extra access being granted.
Per OWASP: Access control enforces policy such that users cannot act outside of their intended permissions. Failures typically lead to unauthorized information disclosure, modification, or destruction of all data or performing a business function outside the user's limits.
This issue affects multiple versions of Apache Tomcat, including 8.5.46 through the final community release 8.5.100 of the NES-supported 8.5.x line.
Module Info
- Product: Apache Tomcat
- Affected packages:
org.apache.tomcat:tomcat-catalina,org.apache.tomcat.embed:tomcat-embed-core - Affected versions: >=8.5.46 <=8.5.100
- GitHub repository: https://github.com/apache/tomcat
- Published packages: https://central.sonatype.com/artifact/org.apache.tomcat/tomcat-catalina
- Package manager: Maven
- Fixed in: NES for Apache Tomcat 8.5.100-tomcat-8.5.110 (upstream fixed in Apache Tomcat 9.0.121, 10.1.59, and 11.0.25)
Vulnerability Info
This Low-severity vulnerability is found in the org.apache.tomcat:tomcat-catalina package in multiple versions of Apache Tomcat, and in the same classes as they are repackaged into org.apache.tomcat.embed:tomcat-embed-core. It arises from two role-resolution features being wired into one another. The first is declarative access control: a <security-constraint> in the deployment descriptor names a URL pattern and an <auth-constraint> listing the roles allowed to reach it, and the container refuses the request unless the authenticated user holds one of those roles. The second is the servlet role reference, a <security-role-ref> that pairs a <role-name> the servlet uses in its own code with a <role-link> naming the application role it really means, so that a servlet written against the name manager can be deployed into an application whose role is called admin. The Servlet specification scopes that alias to HttpServletRequest.isUserInRole(); it is not part of the container's own authorization decision.
In the affected releases, the Realm applied the alias to both. org.apache.catalina.realm.RealmBase.hasResourcePermission() walks the roles listed in the matching constraint and asks the Realm about each one, passing the servlet that the request was mapped to:
for (String role : roles) {
if (hasRole(request.getWrapper(), principal, role)) {and RealmBase.hasRole(Wrapper, Principal, String) rewrites the role it was asked about through that servlet's own aliases before testing it against the user:
@Override
public boolean hasRole(Wrapper wrapper, Principal principal, String role) {
// Check for a role alias
if (wrapper != null) {
String realRole = wrapper.findSecurityReference(role);
if (realRole != null) {
role = realRole;
}
}The rewrite is unconditional in practice, because the lookup it guards on never reports "no alias". org.apache.catalina.core.StandardWrapper.findSecurityReference() falls back to the Context-level role mapping whenever the servlet itself has no entry for the name:
public String findSecurityReference(String name) {
String reference = null;
referencesLock.readLock().lock();
try {
reference = references.get(name);
} finally {
referencesLock.readLock().unlock();
}
// If not specified on the Wrapper, check the Context
if (getParent() instanceof Context) {
Context context = (Context) getParent();
if (reference != null) {
reference = context.findRoleMapping(reference);
} else {
reference = context.findRoleMapping(name);
}
}
return reference;
}StandardContext.findRoleMapping() is documented to return the corresponding role name if there is one and otherwise to return the specified role unchanged, so under a deployed Context this method never returns null. The realRole != null guard in hasRole() therefore always fires, and the role named in the constraint is always replaced by whatever the alias chain resolves to, never merely added to the set of roles that would satisfy the constraint.
The result is a swap. Take a constraint that protects /protected.txt with an <auth-constraint> naming the role manager, and a servlet mapped to that path which declares a <security-role-ref> pairing <role-name>manager</role-name> with <role-link>guest</role-link>. The container asks whether the caller holds manager; the alias rewrites the question to guest; and the answer that comes back decides the request. A user who holds only guest is granted access to a resource that the descriptor reserves for managers, while a user who holds manager and not guest is denied it. Both outcomes were observed directly against an affected 8.5.x build: unpatched, the manager received a 403 and the guest-only user received a 200; with the correction applied, the same two requests returned 200 and 403 respectively. An operator diagnosing this by symptom is therefore as likely to notice the legitimate role failing as the illegitimate one succeeding.
Exposure depends entirely on the deployed application's own descriptor. Three conditions must hold together: the container must perform a declarative check, through a <security-constraint> with an <auth-constraint> matching the request URI or the equivalent @ServletSecurity annotation; the servlet mapped to that URI must declare a <security-role-ref> whose <role-name> is the same string as a role in that constraint; and the attacker must already hold a valid account carrying the linked role. Because the substitution is total, the misconfiguration usually announces itself, since the role the application intended to require stops working at the same moment. The dangerous case is the quieter one, where the constraint role remains satisfiable by some other path and only the widening is visible, or where the linked role is strictly weaker than the role the constraint names. The Apache Tomcat security team rates this issue Low and published no CVSS vector for it. No web application shipped with Apache Tomcat declares a <security-role-ref>, so a default installation carries no exploitable path of its own.
Mitigation
Only recent major lines of Apache Tomcat are community-supported. The affected 8.5.x line reached End-of-Life on March 31, 2024 (final community release 8.5.100) and will not receive public updates to address this issue.
Applications that cannot be updated immediately can close the exposure in their own descriptors, by ensuring that no <security-role-ref> declares a <role-name> that is also used as a role name in any <auth-constraint> reachable through that servlet. Removing the collision, either by dropping the <security-role-ref> where the servlet does not need it or by choosing a <role-name> that no <auth-constraint> uses and updating the servlet's own HttpServletRequest.isUserInRole() calls to match, removes the substitution entirely.
Note that correcting this behaviour changes two observable contracts. First, Wrapper.findSecurityReference() no longer applies the Context-level role mapping and can return null for a name that has no matching <security-role-ref>, where it previously returned a value in every case; third-party Realm or Valve implementations that call it directly will see that difference. Second, the deployment-time role validation performed by ContextConfig registers the raw <role-link> value as a security role rather than the Context-mapped name. Neither change affects HttpServletRequest.isUserInRole(), which continues to resolve the servlet's <security-role-ref> alias and then the Context role mapping exactly as before.
Users of the affected components should apply one of the following mitigations:
- Upgrade to a currently supported Apache Tomcat major line (9.0.x or later) that contains the fix.
- Leverage a commercial support partner like HeroDevs for post-EOL security support.
Credits
- 4ra1n (finder)
- pyn3rd (finder)
- unam4 (finder)