CVE-2025-54576 - Bypassing Cluster Authentication

Editor’s note: Jennifer is a former Recurity Labs employee who now works as a freelancer, continuing to contribute to many of our projects. We are grateful to still have her expertise and insight on board, and her contributions remain highly valued and appreciated. The following post was written by Jennifer.

The correct operation and security auditing of microservice architectures is one of the challenges one faces with modern products. While it is not the only area of application, this design scheme is very common in the context of web applications. This blog post describes how a typical web application audit led to identifying a critical authentication bypass in the widespread OAuth2-Proxy tool.

For those who are primarily interested in the issue’s details, there is also a detailed advisory available on GitHub, filed under the CVE-2025-54576. If you are running OAuth2-Proxy with a regex-based whitelist for public endpoints, you might be affected by this issue.

High Resolution Architectures

Fifteen years ago, even complex web applications were commonly realized by one big monolithic code base. Most prominently, Java-based solutions spread over numerous JAR files, each larger than anyone would like to maintain.

Next came the era of microservices. The business logic of an application is split into meaningful parts, each developed and maintained by a separate team. Gone are the days of digging through hundreds of megabytes of source code. Great!

But wait, that would be too good to be true. Each microservice is now pretty simple on its own, so where did the magic disappear? You might have guessed it: it hides in the correct integration of the services with each other.

The Bigger Picture

Recently, I had the luck to be tasked with a source code audit of various microservices for one of our customers. This is pretty uncommon, since what is developed separately is usually tested separately as well. But didn’t we just say that integration aspects are essential for this type of architecture? This inadvertently leaves out significant parts of the architecture and often results in wrong assumptions about the guarantees another service provides.

I hope that this article will provide you with a high-impact attack scenario, where the exploitability for each involved microservice on its own is pretty limited. So let’s have a look at the setup:

Parts of the authentication for the numerous services were moved to a central component—an instance of the OAuth2-Proxy tool. It can be configured as authentication middleware for a Kubernetes cluster by using annotations such as nginx.ingress.kubernetes.io/auth-signin and nginx.ingress.kubernetes.io/auth-url for the corresponding Ingress resource. In short, incoming requests are checked for valid authentication material and only forwarded to the cluster’s services afterward. If no such material is found, users can automatically be forwarded to a login page.

Actually, this centralized setup was not in scope. On the one hand, it had already been audited shortly before; on the other hand, a source code review targeting custom code—and not an infrastructure audit—was commissioned. But in the end, the objective is still the evaluation of the services’ security, and that is fundamentally linked to this component.

Regex, A Never Ending Story

So, besides reviewing the service-specific source code, I started digging through the Ingress and OAuth2-Proxy configuration. From the usage of the services, I already knew that specific paths were excluded from the access restrictions applied by the OAuth2-Proxy. At some point, I stumbled across definitions like the following:

skip_auth_routes = [
    ...
    "^/service_one/public_endpoint$",
    "^/service_two/.*/status$",
    ...
]

The documentation for this option states the following: Excerpt from OAuth2-Proxy documentation

It is obvious that one will get into huge trouble if the line ^/service_two/.*/status$ is more permissive than expected. From my personal experience, this happens in situations where developers make use of regular expressions with wildcards quite regularly. So, intuition told me to have a short look at the OAuth2-Proxy code base.

The Bug

Ok, so what does the code flow responsible for excluding certain URL paths from the authentication process look like?

The function IsAllowedRequest is responsible for evaluating whether authentication can be skipped for an incoming request. One of the supported cases depends on the skip_auth_routes option and is realized in the ‘isAllowedPath’ function:

func isAllowedPath(req *http.Request, route allowedRoute) bool {
	matches := route.pathRegex.MatchString(requestutil.GetRequestURI(req))

	if route.negate {
		return !matches
	}

	return matches
}

One can observe that the configured regex is applied to the return value of requestutil.GetRequestURI(req) that takes the incoming request as argument.

func GetRequestURI(req *http.Request) string {
	uri := req.Header.Get(XForwardedURI)
	if !IsProxied(req) || uri == "" {
		// Use RequestURI to preserve ?query
		uri = req.URL.RequestURI()
	}
	return uri
}

The included comment should directly set everyone’s alarm bells ringing. It was intended to match the request’s URL path, not the path AND query component. A short inspection of the values expected to be provided by the X-Forwarded-URI HTTP header, and the documentation for the RequestURI function, confirms that this intention is not met.

Let’s go back to our configuration example. Authentication will be skipped not only for requests to URL paths such as /service_two/v1/status, but also for endpoints such as /service_two/admin when one adds a suitable final query parameter, e.g., nonexistingparam=/status. The above implementation will then try to perform the regex match against the full string /service_two/admin?nonexistingparam=/status. Since it is quite common for web applications to ignore unknown query parameters, it is assumed that one can do so without running into problems.

One question remains to be answered, now that requests can be sent to arbitrary endpoints under /service_two/ without being rejected due to missing authentication: How does the final service determine the user context, since we are not authenticated? This depends on the implementations of the final services and cannot be answered universally. The following sections will therefore assume the test environment present in the original audit. Some aspects of this setup are quite common, so maybe you will identify similarities to environments you know.

Hello Security In-Depth

The use case of the OAuth2-Proxy tool is quite security-sensitive, and it has to be positively noted that further exploitation of the authentication bypass was significantly hindered by one of its defense-in-depth measures. To describe this, I first have to briefly depict the specific setup in which the OAuth2-Proxy instance was used (other configurations and behaviors can be set, but were not inspected here).

When a user tried to access one of the endpoints actively managed by the OAuth2-Proxy instance, the following occurred:

The above cases show that the services in the cluster generally expect a JWT as the primary user authentication material. However, the usual behavior of OAuth2-Proxy—providing a valid JWT—is circumvented for some endpoints due to the bypass. The final service still requires some kind of user context and will attempt to extract it from a JWT. So, one might think to simply add a self-signed JWT to the request to get it working, right?

Well, the answer is, of course, no! Skipping authentication for a request does not mean that OAuth2-Proxy will fully ignore the request contents. Dynamic tests revealed that it still inspects the HTTP Authorization header and will only forward its value if the JWT is successfully validated.

This is a really cool fallback behavior to mitigate risks that can also arise from lax configurations.

Legacy + Complexity = Exploit

Please note that this section is deliberately lacking technical details, as these are confidential.

Now that it became apparent that sending JWTs directly was not an option, one had to look for another way to achieve the set goal. Luckily, the request traversed another authentication-related legacy component after being processed by the OAuth2-Proxy and before arriving at the target service. Among other things, this intermediate service took the value of a specific cookie and copied it into the HTTP Authorization header before forwarding the request. This was likely implemented to maintain compatibility with the old, cookie-based world of authentication. It did not do this blindly, however, and required some convincing through the exploitation of further security flaws.

In the end, an authentication bypass by providing arbitrary self-signed JWT payloads to the target service could be achieved. This is where knowledge of the interaction and security guarantees of the different microservices involved in processing a request became key to making the flaw exploitable. The chain involved three services: the OAuth2-Proxy instance, the intermediate authentication component, and the final web service. The web service assumed that any JWT it received had already been verified by the components in front. The OAuth2-Proxy intended to mitigate the risk of skip_auth_routes misconfigurations by removing invalid JWTs, and the intermediate component was already considered superseded with the introduction of OAuth2-Proxy into the cluster. Consequently, when looking at each of these components separately, the actual risk does not become apparent. Some questionable behavior—such as copying a cookie value into the HTTP header—might even appear to be a feature, but in this case, it ultimately turned into an exploitation gadget.

For Take Away

For operators of microservice architectures:

Keeping the bigger picture in mind is crucial for security, but this is often made difficult by the independence of different teams. As external consultants, one often observes that the overarching technical management between teams needs improvement. Documentation, not solely of the functional behavior, is very important. It should also cover the different technical responsibilities and limitations of each component, including everything related to distributed security topics.

As explained above, a lot of complexity shifts from the implementation to integration aspects. This fact is widely overlooked or insufficiently addressed. The same is reflected in commissioned audits that target single services. During these, analyzing a service as part of the bigger picture is very difficult, and often impossible, due to missing information or time constraints.

For auditors:

Trust your instincts where it seems worth digging deeper. One cannot always explain where premonitions come from—certain code patterns are more error-prone than others, or stand out when seen in an unusual context.

It is also good to take a cursory look beyond the explicit scope of your assessment when it seems essential for security. Of course, never perform any dynamic tests without the customer’s approval. 😉 On every complex target, it makes sense to collect certain logical gadgets that, on their own, might not be relevant for security and may just represent unexpected behavior. One never knows when a flaw might be found that can only be exploited with the help of such additional gadgets.