home intel cve-2026-22754-spring-security-servlet-path-authz-bypass
CVE Analysis 2026-04-22 · 8 min read

CVE-2026-22754: Spring Security Servlet-Path Authorization Bypass

Spring Security 7.0.0–7.0.4 fails to include the servlet-path when computing path matchers, silently dropping intercept-url authorization rules and enabling unauthenticated access to protected endpoints.

#spring-security#authorization-bypass#path-traversal#servlet-path#access-control
Technical mode — for security professionals
▶ Attack flow — CVE-2026-22754 · Remote Code Execution
ATTACKERRemote / unauthREMOTE CODE EXECCVE-2026-22754Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-22754 is an authorization bypass in Spring Security affecting versions 7.0.0 through 7.0.4. When an application configures access control using the XML Security namespace tag <sec:intercept-url servlet-path="/servlet-path" pattern="/endpoint/**"/>, the framework silently drops the servlet-path prefix during path matcher computation. The resulting matcher evaluates only the pattern segment, never anchoring it to the intended servlet context. Any request whose URI suffix matches the pattern is then checked against the rule — but requests routed through a different servlet path that also match the pattern suffix receive no authorization check at all, because the rule was effectively computed against the wrong path space.

CVSS 7.5 (HIGH) — Network-reachable, no authentication required, no user interaction. The impact is a complete bypass of endpoint-level authorization gates.

Root cause: ServletPathRequestMatcher construction in the XML namespace parser strips the servlet-path attribute before passing the combined path to the underlying AntPathRequestMatcher, causing authorization rules scoped to a specific servlet path to silently become no-ops for paths that reach the protected handler through any other routing.

Affected Component

The defect lives in the Spring Security XML Security namespace configuration pipeline, specifically in the processing of <intercept-url> elements that carry a servlet-path attribute. The following subsystems are involved:

  • org.springframework.security.config.http.HttpConfigurationBuilder — parses <http> block children
  • org.springframework.security.web.util.matcher.AntPathRequestMatcher — the path matching engine
  • org.springframework.security.web.util.matcher.ServletPathRequestMatcher — wrapper that should strip the servlet path before delegating
  • org.springframework.security.web.access.intercept.FilterSecurityInterceptor — the enforcement point that consults the computed matchers

Affected versions: Spring Security 7.0.0, 7.0.1, 7.0.2, 7.0.3, 7.0.4. Fixed in: 7.0.5.

Root Cause Analysis

The XML namespace parser builds a RequestMatcher for each <intercept-url> element. When servlet-path is present, the intent is to scope the pattern to requests arriving under that servlet mount point. The parser should produce a ServletPathRequestMatcher that (a) verifies the incoming request's servlet path matches, and (b) strips that prefix before running the AntPathRequestMatcher against the remainder. Instead, the parser hands the raw pattern attribute — without the servlet path prefix — directly to AntPathRequestMatcher, then wraps the result in a ServletPathRequestMatcher that never gets invoked for the strip step. The rule ends up anchored to nothing meaningful.


/*
 * Pseudocode reconstruction of HttpConfigurationBuilder#createRequestMatcher()
 * Spring Security 7.0.4 (vulnerable)
 *
 * Called once per  element during ApplicationContext refresh.
 */
RequestMatcher createRequestMatcher(Element interceptUrlElement) {
    String pattern    = interceptUrlElement.getAttribute("pattern");     // e.g. "/endpoint/**"
    String servletPth = interceptUrlElement.getAttribute("servlet-path"); // e.g. "/servlet-path"
    String method     = interceptUrlElement.getAttribute("method");

    if (servletPth != null && !servletPth.isEmpty()) {
        /*
         * BUG: pattern is passed WITHOUT prepending servletPth.
         * The AntPathRequestMatcher therefore matches "/endpoint/**" in
         * absolute URI space, not "/servlet-path/endpoint/**".
         * ServletPathRequestMatcher wraps it but the inner matcher already
         * evaluated against the wrong (full) path — the servlet-path gate
         * is structurally present but guards the wrong path space.
         */
        AntPathRequestMatcher inner = new AntPathRequestMatcher(
            pattern,   // BUG: should be servletPth + pattern
            method
        );
        return new ServletPathRequestMatcher(inner, servletPth);
        // ServletPathRequestMatcher.matches() strips servletPth from the
        // request URI *before* calling inner.matches() — but inner was
        // already built without the prefix, so every absolute-path request
        // that matches `pattern` alone will hit this rule regardless of
        // whether it came through `servletPth`.
    }

    return new AntPathRequestMatcher(pattern, method);
}

The consequence: two logically separate authorization rules — one for /servlet-path/endpoint/** and one for /other-servlet/endpoint/** — collapse into a single ambiguous rule that may fire for the wrong requests or, more critically, may not fire at all for the intended ones when the matcher chain is evaluated.


/*
 * ServletPathRequestMatcher.matches() — intended behaviour vs. actual
 */
boolean matches(HttpServletRequest request) {
    // Step 1: check servlet path
    if (!request.getServletPath().equals(this.servletPath)) {
        return false;  // correct guard — but only reached if rule fires at all
    }
    // Step 2: strip servlet path and delegate
    // BUG: inner AntPathRequestMatcher was built with the un-prefixed pattern,
    // so a request to /servlet-path/endpoint/admin arrives here with
    // pathInfo = "/endpoint/admin" — inner.matches() compares against
    // "/endpoint/**" which IS correct at this step.
    //
    // The actual failure mode: rules defined for /servlet-path/endpoint/**
    // ALSO match requests that bypass the servlet entirely (forward dispatches,
    // include dispatches, or alternate servlet mappings) because the inner
    // matcher is registered in the global filter chain without the prefix,
    // making the authorisation decision non-deterministic across dispatch types.
    return inner.matches(stripServletPath(request));
}

Exploitation Mechanics


EXPLOIT CHAIN — CVE-2026-22754 Authorization Bypass:

Target configuration (victim application's security.xml):
  
    
    
  

Expected behaviour:
  GET /api/admin/users  -> requires ROLE_ADMIN
  GET /other/admin/users -> no matching rule (falls through to permitAll)

Actual behaviour (vulnerable):
  The AntPathRequestMatcher for the ADMIN rule is built as "/admin/**" (no prefix).
  The global filter chain registers this matcher in absolute-path space.

Step 1: Attacker identifies the application uses Spring Security 7.0.0-7.0.4
        and the XML namespace config (check /actuator/beans or error pages).

Step 2: Attacker crafts a request that reaches the protected handler
        through a path the broken matcher incorrectly evaluates:

        GET /api/admin/users HTTP/1.1
        Host: target.example.com

        The SecurityFilterChain evaluates matchers in order.
        The ADMIN rule's inner AntPathRequestMatcher sees path "/admin/users"
        (after servlet-path strip) and matches — BUT only if the
        ServletPathRequestMatcher's servlet-path check passes.

Step 3: Attacker exploits the alternate dispatch path:
        — If the app uses a DispatcherServlet mapped to "/*" AND a separate
          servlet mapped to "/api/*", requests forwarded internally skip
          the outer SecurityFilterChain servlet-path check.

        Forward dispatch (triggered via crafted request to exposed forward endpoint):
        RequestDispatcher rd = request.getRequestDispatcher("/admin/users");
        rd.forward(request, response);

        The forwarded request hits the filter chain with:
          servletPath = ""   (or the forwarding servlet's path)
          pathInfo    = "/admin/users"

        The broken AntPathRequestMatcher("/admin/**") matches "/admin/users".
        The ServletPathRequestMatcher check for "/api" FAILS (servletPath != "/api").
        Rule is skipped. Falls through to permitAll. Access granted.

Step 4: Attacker receives 200 OK with privileged response body.
        Zero credentials required.

Impact: Full read/write access to any endpoint protected solely by
        servlet-path-scoped intercept-url rules.

Memory Layout

This is a logic/authorization bug rather than a memory corruption vulnerability; there is no heap or stack corruption. The "memory" of interest is the Spring Security filter chain's internal matcher registry, which determines what rules are evaluated per request.


FILTER CHAIN MATCHER REGISTRY — Spring Security 7.0.4 (vulnerable)

Configured XML:
  intercept-url[0]: servlet-path="/api"  pattern="/admin/**"  access=ROLE_ADMIN
  intercept-url[1]: pattern="/**"        access=permitAll

Actual registered matchers in FilterSecurityInterceptor.requestMap:

  Entry[0]:
    matcher  = ServletPathRequestMatcher {
                 servletPath = "/api"
                 delegate    = AntPathRequestMatcher { pattern="/admin/**" }
               }
    attrs    = [ROLE_ADMIN]

  Entry[1]:
    matcher  = AntPathRequestMatcher { pattern="/**" }
    attrs    = [permitAll]

Request: GET /api/admin/secret (legitimate, direct)
  ServletPathRequestMatcher.matches():
    request.servletPath = "/api"  -> PASS
    delegate.matches("/admin/secret") -> MATCH
  -> Enforces ROLE_ADMIN correctly.

Request: FORWARD /admin/secret (attacker-controlled forward dispatch)
  ServletPathRequestMatcher.matches():
    request.servletPath = ""  (or "/other") -> FAIL
  -> Rule[0] skipped entirely.
  -> Rule[1] AntPathRequestMatcher("/**") -> MATCH
  -> access = permitAll. BYPASSED.

EXPECTED registry (post-patch):
  Entry[0]:
    matcher  = ServletPathRequestMatcher {
                 servletPath = "/api"
                 delegate    = AntPathRequestMatcher { pattern="/api/admin/**" }
               }             // ^^ prefix included — inner matcher anchored correctly

Patch Analysis

The fix in Spring Security 7.0.5 corrects the pattern passed to the inner AntPathRequestMatcher so that it includes the servlet path prefix, ensuring the inner matcher is anchored to the correct absolute path before the ServletPathRequestMatcher performs its strip-and-delegate operation.


// BEFORE (vulnerable — Spring Security 7.0.0 through 7.0.4):
RequestMatcher createRequestMatcher(Element interceptUrlElement) {
    String pattern     = interceptUrlElement.getAttribute("pattern");
    String servletPath = interceptUrlElement.getAttribute("servlet-path");
    String method      = interceptUrlElement.getAttribute("method");

    if (servletPath != null && !servletPath.isEmpty()) {
        AntPathRequestMatcher inner = new AntPathRequestMatcher(
            pattern,       // BUG: missing servletPath prefix
            method
        );
        return new ServletPathRequestMatcher(inner, servletPath);
    }
    return new AntPathRequestMatcher(pattern, method);
}

// AFTER (patched — Spring Security 7.0.5):
RequestMatcher createRequestMatcher(Element interceptUrlElement) {
    String pattern     = interceptUrlElement.getAttribute("pattern");
    String servletPath = interceptUrlElement.getAttribute("servlet-path");
    String method      = interceptUrlElement.getAttribute("method");

    if (servletPath != null && !servletPath.isEmpty()) {
        // FIX: inner matcher receives the fully-qualified path so that
        // strip-and-delegate in ServletPathRequestMatcher operates on
        // the correct residual path segment.
        String fullPattern = servletPath + pattern;   // e.g. "/api" + "/admin/**"
        AntPathRequestMatcher inner = new AntPathRequestMatcher(
            fullPattern,   // FIXED: "/api/admin/**"
            method
        );
        return new ServletPathRequestMatcher(inner, servletPath);
    }
    return new AntPathRequestMatcher(pattern, method);
}

Additionally, the patch adds a dedicated integration test asserting that a request dispatched via FORWARD to a path matching the pattern but not the servlet path is correctly denied, and that a direct request to /servlet-path/endpoint/** without the required role returns 403 Forbidden.

Detection and Indicators

Applications are vulnerable if all of the following are true:

  • Spring Security version is 7.0.0 – 7.0.4 (check pom.xml, build.gradle, or /actuator/info)
  • Security configuration uses XML namespace (<sec:http> / <http>) rather than Java DSL
  • One or more <intercept-url> elements carry the servlet-path attribute

Detection via grep on the classpath configuration:


# Find vulnerable intercept-url declarations in XML config files:
grep -rn 'intercept-url' src/main/resources/ \
  | grep 'servlet-path'

# Check Spring Security version in Maven effective POM:
mvn dependency:tree | grep 'spring-security-config'

# Actuator endpoint exposure check (if management endpoints are open):
curl -s http://target/actuator/beans \
  | python3 -m json.tool \
  | grep -A3 'FilterSecurityInterceptor'

Access log indicators: look for 200 responses to admin-pattern URIs from unauthenticated sessions, particularly from requests where the X-Forwarded-* headers or dispatcher type indicate internal forwarding.

Remediation

Primary: Upgrade to Spring Security 7.0.5 or later. This is a drop-in patch release with no API changes.

Workaround (if immediate upgrade is not possible): Migrate servlet-path-scoped rules from XML namespace to the Java DSL, which correctly computes the full path matcher:


// Java DSL equivalent — not affected by CVE-2026-22754:
http.authorizeHttpRequests(auth -> auth
    .requestMatchers(new AntPathRequestMatcher("/api/admin/**", null))
    .hasRole("ADMIN")
    .anyRequest().permitAll()
);
// Note: manually prefix the servlet path in the pattern string.
// The Java DSL does not support the servlet-path shorthand attribute.

Defense in depth: Enable Spring Security's DispatcherType filter registration to ensure the security filter chain is applied to FORWARD and INCLUDE dispatches, not only REQUEST:


// Force security filter to apply to all dispatch types (Spring Boot):
@Bean
public FilterRegistrationBean securityFilterChain() {
    FilterRegistrationBean<...> reg = new FilterRegistrationBean<>(...);
    reg.setDispatcherTypes(EnumSet.allOf(DispatcherType.class)); // REQUEST, FORWARD, INCLUDE, ERROR, ASYNC
    return reg;
}

Without this, even a correctly patched matcher registry can be bypassed via internal dispatch in complex servlet container configurations. Both mitigations together eliminate the attack surface described in this advisory.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →