infiltr8 Lab
Home/ CVE/ 웹·서버/ WSO2 API Manager JWT Signature Verification Bypass
CVE · 웹·서버 · 인증우회 · CVE-2026-5430
심층 분석 연구 분석

CVE-2026-5430 (WSO2 API Manager JWT Signature Verification Bypass) analysis

REST API authentication in WSO2 API Manager and three sibling products silently swallows the exception raised by an unsupported JWT signing algorithm, letting the request through unauthenticated (CVSS 3.1 = 10.0 / 9.8). This piece reconstructs the vendor advisory and reads the vulnerable source and the patch diff directly to explain, at the code level, why a signature check that clearly rejects the token still fails to stop the request.

The code excerpts in this piece (JWTUtil.java, OAuthAuthenticationInterceptor.java, GlobalThrowableMapper.java, OAuthJwtAuthenticatorImpl.java, JWTValidatorImpl.java, OAuthAuthenticationInterceptorException.java) are the actual source of the wso2/carbon-apimgt open-source repository (Apache License 2.0). The pre-patch code was diffed line-by-line against the first parent commit (52ae133a3264e5485b26cf015147533ccddeeda6) of the fix's merge commit (4914da24315062f383bf9c729971d8338df6529b, PR #13752), fetched directly from GitHub raw on 2026-09-17; the post-patch code was taken from the merge commit itself. The patch diff was pulled as-is from the GitHub API (pulls/13752/files). One link in the chain was not traced end-to-end in this session: exactly which downstream REST resource code ultimately trusts the forged token's admin claim once the request slips past the interceptor. That connection is reconstructed from WSO2's own advisory and watchTowr/press reporting, i.e. it rests on secondary sources — we flag that explicitly. The CVSS score was not copied from the advisory; it was independently recomputed from the CVSS 3.1 formula in this session and confirmed to match.

infiltr8 team Original analysis #jwt-alg-confusion#exception-swallowing#auth-bypass#wso2-apim
WhatThe REST API authentication used by WSO2 API Manager, API Control Plane, Traffic Manager and Universal Gateway can be bypassed because the interceptor swallows the exception raised when a JWT is signed with an unsupported algorithm (CVSS 3.1 = 9.8 single-tenant / 10.0 multi-tenant).
MechanismJWTUtil, the code that verifies the signature, correctly throws when it meets an unexpected algorithm. The problem is that the interceptor catching that exception only logs it and returns normally, without re-throwing anything.
Core issueWhen the interceptor returns without throwing, Apache CXF's interceptor chain treats it as "nothing blocked it, so let it through" and proceeds straight to the actual REST resource method — an unverified token carrying a forged admin claim reaches administrative operations untouched.

01개요

CVE-2026-5430 is a flaw in the JWT (JSON Web Token) authentication used by WSO2 API Manager's REST APIs (Admin/Publisher/DevPortal, etc.): when a token is signed with an unsupported algorithm, the resulting exception never gets turned into a rejected request — the authentication interceptor lets it through instead. WSO2's own advisory number for this issue is WSO2-2026-5328, a vendor-internal identifier distinct from the CVE number (CVE-2026-5430).

An attacker needs no account and no login. Using any algorithm other than RS256/RS384/RS512, they can self-sign a forged JWT carrying an admin claim. Once that token slides past authentication, the reachable surface — in watchTowr's words — includes "every API backend endpoint and its credentials, consumer keys, and secrets."

The flaw was found by Hacktron Team; WSO2 published its advisory on 2026-05-03. On 2026-09-13, threat-intel firm watchTowr's honeypot network captured forged JWTs carrying admin privileges arriving in the wild, confirming active in-the-wild exploitation.

CVSS: 9.8 (single-tenant, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) / 10.0 (multi-tenant, Scope Changed — S:C)

WSO2's own advisory does not list a CWE. Most press coverage classifies it as CWE-347 (Improper Verification of Cryptographic Signature). The more precise root cause this piece derives directly from the source — CWE-703, improper handling of an exceptional condition — is covered in §04 below.

02영향받는 버전

대상취약 버전안전 버전
WSO2 API Manager 4.6.0below update level 21update level 21+
WSO2 API Manager 4.5.0below update level 57update level 57+
WSO2 API Manager 4.4.0below update level 72update level 72+
WSO2 API Manager 4.3.0below update level 108update level 108+
WSO2 API Manager 4.2.0below update level 197update level 197+
WSO2 API Manager 4.1.0below update level 257update level 257+
WSO2 API Control Plane 4.6.0below update level 22update level 22+
WSO2 API Control Plane 4.5.0below update level 58update level 58+
WSO2 Traffic Manager 4.6.0below update level 21update level 21+
WSO2 Traffic Manager 4.5.0below update level 56update level 56+
WSO2 Universal Gateway 4.6.0below update level 21update level 21+
WSO2 Universal Gateway 4.5.0below update level 57update level 57+

03Background — the CXF interceptor chain, and a default of "unless told to block, let it through"

WSO2 API Manager's administrative REST APIs (Admin, Publisher, DevPortal, etc.) run on Apache CXF's JAX-RS implementation. Before invoking the actual resource method (e.g. the implementation behind a "create user" endpoint), CXF walks the request through a series of interceptors registered at different phases — authentication is handled by OAuthAuthenticationInterceptor, registered at the PRE_INVOKE phase.

This interceptor extends AbstractPhaseInterceptor, and how that base class behaves is the load-bearing assumption behind this vulnerability: unless handleMessage() explicitly throws or aborts the chain, CXF treats the interceptor as having passed and proceeds to the next stage — ultimately, the actual resource method call. In other words, this design is not "block when in doubt" (fail-closed); it is "let it through unless told to block" (fail-open).

Authentication itself works like this: OAuthJwtAuthenticatorImpl.authenticate() reads the request's Authorization: Bearer <JWT> header and delegates signature verification to JWTValidatorImpl.validateSignature(), which calls JWTUtil.verifyTokenSignature(). A JWT's header self-declares the signing algorithm (alg); WSO2 only trusts RSA-family signatures (RS256/RS384/RS512) — anything else (symmetric HS256, unsigned none, etc.) is supposed to be rejected.

Put together, this API's safety hinges on exactly one thing: when the algorithm check fails, does that failure actually stop the CXF chain? The next section shows where that link breaks.

04취약점 발생 원인

This vulnerability isn't caused by a sloppy signature check — it's caused by mishandling what happens when that check fails. Three weaknesses interlock in sequence: the verification logic (W1) genuinely rejects a bad algorithm, but the interceptor receiving that rejection (W2) erases the signal, and that erasure turns into a full bypass only because of how the CXF interceptor chain is designed by default (W3).

1"Reject anything that isn't RSA" — the signature check itself genuinely rejects a bad algorithm

핵심

JWTUtil.verifyTokenSignature(jwt, alias) throws when the token header's alg isn't RS256/RS384/RS512 — a correct rejection. The problem isn't this method; it's who receives that exception and what they do with it.

Before it ever checks a cryptographic signature, this method looks up a certificate from the truststore and inspects the JWT header's algorithm. If it isn't RSA-family, it never even reaches verifyTokenSignature(jwt, publicKey) (the actual cryptographic check) — it throws an APIManagementException right there. On its own, this is correct defensive behavior.

public static boolean verifyTokenSignature(SignedJWT jwt, String alias) throws APIManagementException { Certificate publicCert = null; //Read the client-truststore.jks into a KeyStore try { publicCert = APIUtil.getCertificateFromParentTrustStore(alias); } catch (APIManagementException e) { throw new APIManagementException("Error retrieving certificate from truststore ",e); } if (publicCert != null) { JWSAlgorithm algorithm = jwt.getHeader().getAlgorithm(); if ((JWSAlgorithm.RS256.equals(algorithm) || JWSAlgorithm.RS512.equals(algorithm) || JWSAlgorithm.RS384.equals(algorithm))) { return verifyTokenSignature(jwt, (RSAPublicKey) publicCert.getPublicKey()); } else { log.error("Public key is not RSA"); throw new APIManagementException("Public key is not RSA"); } } else { log.error("Couldn't find a public certificate with alias " + alias + " to verify the signature"); throw new APIManagementException( "Couldn't find a public certificate with alias " + alias + " to verify the signature"); } }

Actual wso2/carbon-apimgt source (pre-patch) — JWTUtil.java:107-131 · verifyTokenSignature(SignedJWT, String) (parent commit 52ae133).

왜 이렇게 됐나Taken in isolation, this method is fine — anything outside RSA-family algorithms is rejected before real cryptographic verification even starts. The highlighted lines (122-125) are exactly that rejection point. The next section's question is: does whoever catches this exception actually stop the request?

2"An exception was thrown, so it must have been rejected" — the interceptor catches it and moves on

핵심

OAuthAuthenticationInterceptor.handleMessage() catches the APIManagementException bubbling up from the authentication chain, but instead of turning it into a rejected request, it only logs it and returns.

W1's exception doesn't reach this interceptor directly — OAuthJwtAuthenticatorImpl.authenticate() calls JWTValidatorImpl.validateSignature(), which calls JWTUtil.verifyTokenSignature(), and none of the methods along that call path absorb the APIManagementException; it propagates upward untouched. The first place that actually catches it is this interceptor's handleMessage().

if (authenticator.authenticate(authContext)) { inMessage = JWTAuthenticationUtils.addToMessageContext(inMessage, authContext); if (logger.isDebugEnabled()) { logger.debug("Request has been Authenticated , authentication type : "+ authenticationType); } logAuditOperation(inMessage); } else { logger.error("Failed to Authenticate , authentication type : "+ authenticationType); throw new AuthenticationException("Unauthenticated request"); } } catch (APIManagementException e) { logger.error("Authentication Failure " + e.getMessage()); return; }

Actual wso2/carbon-apimgt source (pre-patch) — OAuthAuthenticationInterceptor.java:98-111 · handleMessage() (parent commit 52ae133).

왜 이렇게 됐나The branch right above (line 106), throw new AuthenticationException(...), is correctly mapped to HTTP 401 by GlobalThrowableMapper (next section). But the catch block right below it (lines 108-110) — handling the exact same underlying situation, "authentication failed" — only logs and swallows it. When handleMessage() returns normally without throwing anything, CXF's PRE_INVOKE interceptor chain treats this interceptor as having passed and proceeds. Worth noting: the same file has a second authentication path used when authenticator == null (around lines 147-149, the default JWT/opaque path through authenticatorMap — the actual vulnerable path for this CVE), and it carried the identical pattern (log only, never re-throw). The patch fixes both catch blocks.

3"Unless blocked, let it through" — the safety net existed, but the signal never reached it

핵심

GlobalThrowableMapper already had correct logic to turn AuthenticationException and APIManagementException into proper HTTP errors. The problem is that W2 never re-threw anything to trigger that mapping.

In CXF's JAX-RS layer, when an exception is actually thrown — whether during resource method invocation or, as here, during interceptor processing — the ExceptionMapper GlobalThrowableMapper.toResponse() catches it and converts it into the right HTTP status. Below is the branch that maps authentication failure to 401 — this code was not touched by the patch (it already worked correctly).

if (e instanceof AuthenticationException) { ErrorDTO errorDetail = new ErrorDTO(); errorDetail.setCode((long)401); errorDetail.setMoreInfo(""); errorDetail.setMessage(""); errorDetail.setDescription(e.getMessage()); return Response .status(Response.Status.UNAUTHORIZED) .type(MediaType.APPLICATION_JSON_TYPE) .entity(errorDetail) .build(); }

Actual wso2/carbon-apimgt source (identical pre- and post-patch — this branch was not changed in this commit) — GlobalThrowableMapper.java:128-139.

왜 이렇게 됐나This mapping logic was correct from the start — anything that reached this code as an AuthenticationException or APIManagementException would have gotten a clean 401. But because the PRE_INVOKE interceptor chain defaults to fail-open ("let it through unless explicitly told to block"), the moment W2 fails to re-throw anything, this safety net never gets a chance to fire. The net wasn't missing — the switch that turns it on was never flipped.

Analysis verdict

The center of gravity is W2 — the interceptor swallowing the exception — and nothing else.

W1 (algorithm validation) and W3 (HTTP error mapping infrastructure) are each sound designs on their own — "reject unexpected algorithms with an exception" and "map authentication exceptions to 401" are both correct. W2 cuts the signal in between, so the rejection W1 generates never reaches W3.

That's exactly what the patch repairs — a new exception type (OAuthAuthenticationInterceptorException) lets W2 re-throw, and one added condition lets W3 (GlobalThrowableMapper) recognize that new type too. W1's algorithm-check logic itself is untouched.

05공격 과정

5.1 공격 과정 요약

공격 흐름도
공격 흐름도 (직접 작성)

5.2 상세 공격 과정

This differs from the usual "attacker forged a valid signature" story — the attacker never actually deceives the signature check. They don't need to produce a real RSA signature at all. Confirmed In the code path we directly diffed, the algorithm check (§04, W1) throws before the signature bytes are even inspected — all that's required is a syntactically valid three-segment token whose alg value isn't RS256/384/512.

Analysis From the call order we traced in OAuthJwtAuthenticatorImpl.authenticate(), signature verification (validateSignature) runs before handleScopeValidation(), which is what reads the scope claim. That means a request riding this bypass path may reach the resource method with scope validation never having executed at all — the "forged token with an admin claim" that WSO2 and watchTowr describe is the concrete method actually used in the observed attacks, but the code we diffed suggests an even looser condition (the complete absence of scope checking) may theoretically have been sufficient. This last sentence is our own inference, not traced all the way through the resource implementation in this session, and not something WSO2 separately confirmed.

The exact header/payload bytes of a working forged token are not published here (harm-reduction policy) — this piece stays at the conceptual level of "what condition triggers this path."

5.3 미흡점 ↔ 공격 흐름 매핑

미흡점(§4)발화 지점(§5.2)여는 프리미티브→ 다음으로 잇는 것
W1 — algorithm check (correct rejection)JWTUtil.verifyTokenSignature() call (§8.2 step 3)Rejection signal (APIManagementException) raised for a non-RSA algorithmException propagates up the call stack, unabsorbed, to the interceptor
W2 — interceptor swallows the exceptioncatch block in OAuthAuthenticationInterceptor.handleMessage() (§8.2 step 4)Rejection signal is only logged; nothing is re-thrownhandleMessage() returns normally, without an exception
W3 — CXF chain's fail-open defaultPRE_INVOKE interceptor chain's pass/fail decision (§8.2 step 5)"Unless told to block, let it through" interprets the erased signal as approvalRequest carrying the forged admin claim reaches the actual resource method

06취약점 패치

OAuthAuthenticationInterceptor.java·handleMessage() — both catch(APIManagementException) blocks·PR #13752, merge commit 4914da24315062f383bf9c729971d8338df6529b (2026-04-12)
logger.error("Authentication Failure " + e.getMessage()); return; String errorMsg = "Error while authenticating incoming request"; logger.error(errorMsg, e); throw new OAuthAuthenticationInterceptorException(errorMsg, e); ( the second catch block — in the authenticator == null branch, the actual vulnerable path — is patched identically ) logger.error("Error while authenticating incoming request to API Manager REST API", e); String errorMsg = "Error while authenticating incoming request to API Manager REST API"; logger.error(errorMsg, e); throw new OAuthAuthenticationInterceptorException(errorMsg, e);

왜Because W2 — catching the exception and simply returning/proceeding — was the actual root cause. The key fact is that if an APIManagementException from the authentication chain isn't re-thrown, the CXF PRE_INVOKE interceptor chain keeps the request moving.

어떻게① A new exception type, OAuthAuthenticationInterceptorException (extends WebApplicationException), is introduced. ② Both catch blocks, which used to only log, now throw this exception instead. Because WebApplicationException is a standard JAX-RS exception, CXF recognizes it as an explicit request failure and aborts the interceptor chain. ③ GlobalThrowableMapper.toResponse()'s condition is widened to e instanceof APIManagementException || e instanceof OAuthAuthenticationInterceptorException, so ExceptionUtils.getThrowableList() can walk back to the original APIManagementException in the new exception's cause (e.g. the one JWTUtil threw) and map it to the correct HTTP status. ④ JWTUtil.java separately attaches ExceptionCodes.ACCESS_TOKEN_INVALID to the "Public key is not RSA" exception, so step ③'s mapping resolves cleanly to 401 (authentication failure) — without this, the exception would be thrown but carry no error code, and the mapping wouldn't land where intended.

비고The commit message and PR title are both "Improve exception handling." None of the four files this PR touches (JWTUtil.java, OAuthAuthenticationInterceptor.java, GlobalThrowableMapper.java, and the new OAuthAuthenticationInterceptorException.java) change the signature-verification algorithm itself — exactly as the title says, the real cause was exception-handling flow, not cryptography. The sibling repository product-apim's PR #14167 is not a code fix at all; it only updates an integration test case (AdvancedConfigurationsTestCase.java).

07대응방안

Updating to the update level listed per product in the table above is the only real fix. Community users can apply the fixes in GitHub PR #13752/#14167 directly or upgrade to a later release; WSO2 support subscribers should update to the listed update level or higher via WSO2 Update Manager.

08왜 이게 반복되는가

This case and the Issabel Framework hard-coded JWT key CVE we covered alongside it (CVE-2026-89026) both end at the same outcome — a forged JWT stealing administrative access — but for opposite root causes. Issabel's problem was that the key itself was public, making signature verification meaningless from the start (CWE-321); WSO2's verification logic and its error-mapping infrastructure were each individually correct, but a single piece of exception handling in between cut the signal (CWE-703). "We verify the signature" is not, by itself, a safety guarantee — what you do when that verification fails is part of the same sentence, and these two cases show that from opposite directions.

Catching a checked exception, logging it, and moving on is common in Java codebases — usually harmless. It stops being harmless the moment that exception is the vehicle for a security decision ("this request must be rejected"). When the surrounding authentication framework defaults to fail-open — "let it through unless explicitly blocked" — one missing re-throw in one catch block is enough to erase the whole authentication step.

Patch-completeness assessment. ① Did it remove the root cause? Confirmed — both catch blocks in the same file were fixed identically, and no point remains where the signal is silently erased. ② Variant surface? Analysis — searching the repository for other callers of JWTUtil.verifyTokenSignature-style methods turns up the API gateway (data-plane) side too, in GatewayUtils.java and InternalAPIKeyAuthenticator.java (the Internal Key/MCP authentication path). We did not trace all the way through in this session whether that path shares the same "caught but never explicitly rejected" pattern. This is a static signal only, logged as a separate patch-gap candidate rather than a confirmed finding. ③ Silent or incomplete re-patch? Analysis — product-apim's PR #14167 is only an integration-test update, not evidence that a re-patch was needed, and no follow-up commit revisiting this fix was found. Taken together, this patch is judged to remove the root cause for the REST API interceptor path; the gateway-side Internal Key path remains an open hypothesis that needs separate verification.

The signature-verification logic was fine. The infrastructure that turns errors into HTTP codes was fine. The problem sat between the two — the single channel that's supposed to carry a failure forward as a failure. So what we're leaving behind isn't a tool, it's this question: is this exception actually blocking the request?

09결론

CVE-2026-5430 is an authentication bypass in the REST API authentication of WSO2 API Manager and three sibling products: the exception raised by a JWT signed with an unsupported algorithm is logged and swallowed by the interceptor instead of rejecting the request (CVSS 3.1 = 9.8-10.0). Both the signature-verification logic and the HTTP error-mapping infrastructure were individually correct, but two catch blocks in between erased the rejection signal, letting the CXF interceptor chain's "unless blocked, let it through" default carry the request all the way to a privileged operation. The patch restores that signal by introducing a new exception type and widening the mapping condition. watchTowr observed active exploitation on 2026-09-13.

참고 자료

The code excerpts in this piece (JWTUtil.java, OAuthAuthenticationInterceptor.java, GlobalThrowableMapper.java, OAuthJwtAuthenticatorImpl.java, JWTValidatorImpl.java, OAuthAuthenticationInterceptorException.java) are the actual source of the wso2/carbon-apimgt open-source repository (Apache License 2.0). The pre-patch code was diffed line-by-line against the first parent commit (52ae133a3264e5485b26cf015147533ccddeeda6) of the fix's merge commit (4914da24315062f383bf9c729971d8338df6529b, PR #13752), fetched directly from GitHub raw on 2026-09-17; the post-patch code was taken from the merge commit itself. The patch diff was pulled as-is from the GitHub API (pulls/13752/files). One link in the chain was not traced end-to-end in this session: exactly which downstream REST resource code ultimately trusts the forged token's admin claim once the request slips past the interceptor. That connection is reconstructed from WSO2's own advisory and watchTowr/press reporting, i.e. it rests on secondary sources — we flag that explicitly. The CVSS score was not copied from the advisory; it was independently recomputed from the CVSS 3.1 formula in this session and confirmed to match.