Spring WebClient / Reactor-netty broken when a server responds early? - spring

I don't know much about Webflux / Reactor / Netty. I'm using Spring's WebClient to do all the heavy lifting. But it appears not to work correctly when a server responds back early with an error.
My understanding is when you are POSTing data to a server, the server can respond at any time with an HTTP 4XX error. The client is supposed to stop sending the HTTP body and read that error.
I have a very simply WebClient that POSTs data to a server. It looks like this:
FileResponse resp = client.post().uri(uri)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("Authorization", "Bearer " + authorizationToken)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(data)
.retrieve()
.bodyToMono(FileResponse.class)
.block();
The body can contain a large amount of data (100+KB). Apparently the server looks at the header, validates the authorization token, and only if it's valid, reads the body. If the authorization token is not valid (expired, etc) it immediately responds with an "HTTP 401 Unauthorized" with the response body "{"message": "Invalid user/password"}" while the client is still sending the body. The server then closes the socket which results in the WebClient throwing this:
2022-08-10 15:56:03,474 WARN [reactor.netty.http.client.HttpClientConnect] (reactor-http-nio-1) [id: 0xa7b48bb8, L:/5.6.7.8:51122 - R:dubcleoa030/1.2.3.4:5443] The connection observed an error: java.io.IOException: An existing connection was forcibly closed by the remote host
at java.base/sun.nio.ch.SocketDispatcher.read0(Native Method)
at java.base/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at java.base/sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:276)
at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:233)
at java.base/sun.nio.ch.IOUtil.read(IOUtil.java:223)
at java.base/sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:358)
at deployment.bp-global.war//io.netty.buffer.PooledByteBuf.setBytes(PooledByteBuf.java:253)
at deployment.bp-global.war//io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:1133)
at deployment.bp-global.war//io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:350)
at deployment.bp-global.war//io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:148)
at deployment.bp-global.war//io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
at deployment.bp-global.war//io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
at deployment.bp-global.war//io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
at deployment.bp-global.war//io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
at deployment.bp-global.war//io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
at deployment.bp-global.war//io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at deployment.bp-global.war//io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:834)
I've made the same request with curl, and it's handled properly. Curl sees the server's early response, stops sending the body and processes the response from the server. I've chopped out a lot of fluff from the curl output but here is the important stuff...
Trying 1.2.3.4...
* TCP_NODELAY set
* Connected to 1.2.3.4 port 5443 (#0)
> POST /api/folders/file/?path=/out HTTP/1.1
> Host: 1.2.3.4:5443
> User-Agent: curl/7.61.1
> accept-encoding: gzip
> Content-Type: application/octet-stream
> Authorization: Bearer youshallnotpass
> accept: application/json
> Content-Length: 298190
>
< HTTP/1.1 401 Unauthorized
< Server: Cleo Harmony/5.7.0.3 (Linux)
< Date: Wed, 10 Aug 2022 21:59:23 GMT
< Content-Length: 36
< Content-Language: en
< Content-Type: application/json
< Connection: keep-alive
* HTTP error before end of send, stop sending
* Closing connection 0
{"message": "Invalid user/password"}
I'm not sure if the issue is with Spring's WebClient or the underlying reactor-netty stuff. But am I crazy or does it just look broken if the server responds early? If I am correct that it's broken, any thoughts on a work-around?
Thank you!
Todd

I setup a small stand-alone command line Spring Boot program so I could test various aspects of this issue. What I found is that if I send the body from memory (byte[]) the issue occurs. If I send the body from a file resource as shown below, everything works correctly.
Our current very large product is using Spring Boot 2.3.3. My stand-alone test program gave me the ability to quickly upgrade Spring Boot to 2.7.2. Everything works correctly in Spring Boot 2.7.2. So it was definitely a bug that was fixed at some point.
Unfortunately our large project cannot be upgraded to Spring Boot 2.7.2 overnight. It will be a large effort which will require extensive testing from our QA department. So for now, as a work-around, I'll write the body payload to a temporary file so I can get WebClient to read it as a file resource which works in 2.3.3 as shown below. Just in case any other poor developer runs into this and needs a work-around, try this...
InputStreamResource resource = new InputStreamResource(new FileInputStream(tempFilename));
String resp = client.post().uri(uri)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("Authorization", "Bearer youshallnotpass")
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromResource(resource))
.retrieve()
.bodyToMono(String.class)
.block();
EDIT: I started going through the releases to determine when this was fixed. This issues appears to be fixed in Spring Boot 2.3.4 (Netty 4.1.52). I cannot find any reference to this bug in the release notes of Spring Boot or Netty. So I'm not sure where the issue was. Perhaps it was in a deeper dependency. But it was definitely fixed with Spring Boot 2.3.4.

It may happened because there is a conflict in dependencies (may create conflict with azure dependency for same package):
update your spring boot version I ahve updted to latest and works for me.

Related

Prevent Open URL Redirect from gorilla/mux

I am working on a RESTful web application using Go + gorilla/mux v1.4 framework. Some basic security testing after a release revealed an Open URL Redirection vulnerability in the app that allows user to submit a specially crafted request with an external URL that causes server to response with a 301 redirect.
I tested this using Burp Suite and found that any request that redirects to an external URL in the app seems to be responding with a 301 Moved Permanently. I've been looking at all possible ways to intercept these requests before the 301 is sent but this behavior seems to be baked into the net/http server implementation.
Here is the raw request sent to the server (myapp.mycompany.com:8000):
GET http://evilwebsite.com HTTP/1.1
Accept: */*
Cache-Control: no-cache
Host: myapp.mycompany.com:8000
Content-Length: 0
And the response any time is:
HTTP/1.1 301 Moved Permanently
Location: http://evilwebsite.com/
Date: Fri, 13 Mar 2020 08:55:24 GMT
Content-Length: 0
Despite putting in checks for the request.URL to prevent this type of redirect in the http.handler, I haven't had any luck getting the request to reach the handler. It appears that the base http webserver is performing the redirect without allowing it to reach my custom handler code as defined in the PathPrefix("/").Handler code.
My goal is to ensure the application returns a 404-Not Found or 400-Bad Request for such requests. Has anybody else faced this scenario with gorilla/mux. I tried the same with a Jetty web app and found it returned a perfectly valid 404. I've been at this for a couple of days now and could really use some ideas.
This is not the claimed Open URL redirect security issue. This request is invalid in that the path contains an absolute URL with a different domain than the Host header. No sane client (i.e. browser) can be lured into issuing such an invalid request in the first place and thus there is no actual attack vector.
Sure, a custom client could be created to submit such a request. But a custom client could also be made to interpret the servers response in a non-standard way or visit a malicious URL directly without even contacting your server. This means in this case the client itself would be the problem and not the servers response.

Jetty upgrade (9.2.24 -> 9.4.10) fails with websocket (UpgradeException)

I am in the process of upgrading our jetty from 9.2.24 to 9.4.10, for an app that works extensively with websockets.
I have an existing test (junit) that sets embedded jetty, registers to it rest resource and websocket servlet and then tests to see if they can be accessed.
The test works perfectly well when jetty is at version 9.2.24. An attempt to move to version 9.4.10 with the very same code fails with
java.io.IOException: Connect failure
at org.eclipse.jetty.websocket.jsr356.ClientContainer.connect(ClientContainer.java:232)
at org.eclipse.jetty.websocket.jsr356.ClientContainer.connectToServer(ClientContainer.java:255)
...
Caused by: org.eclipse.jetty.websocket.api.UpgradeException: 400 Bad Request
at org.eclipse.jetty.websocket.client.WebSocketUpgradeRequest.onComplete(WebSocketUpgradeRequest.java:522)
at org.eclipse.jetty.client.ResponseNotifier.notifyComplete(ResponseNotifier.java:193)
The websocket definition on server side is based o JSR356 spec (i.e. extends EndPoint). The websocket client used to access the websocket is also based on the javax.websocket (i.e. ContainerProvider.getWebSocketContainer().connectToServer(Endpoint instance...)) - where the websocket container is effectively a jetty one...
The server sets up perfectly. The problem is only when trying to access the websocket. I have debugged and could not find any difference in the way the client initiates and sends the websocket request. In particular the request has a the 'upgrade' header set to 'websocket' as expected.
So I could only assume that the problem is in the way the websocket resource is registered in the embedded jetty. I have debugged the working flow (with 9.2.24) and found the most early place where the connection is accepted in jetty (one of the selector threads at AbstractConnection). but from some reason I am not getting to that point for the websocket when working with 9.4.10
I have read several resources and SO question (e.g. this question) and could not found anything that will help me with this problem.
I am in a dead end.
Here is the key elements in the relevant code of the server registration (I also have another rest resource along with the websocket one):
// web socket
ServletContextHandler wsContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
wsContext.setContextPath("/ws_api");
ServerContainer container = WebSocketServerContainerInitializer.configureContext(servletContextHandler);
container.addEndpoint(new BasicServerEndpointConfig(container.getClient(), endpointClassObject, path)
// rest handler
ServletContextHandler restContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
restContext.setContextPath("/rest_api");
...
ServletHolder sh = new ServletHolder(...);
restContext.addServlet(sh, "/*");
final HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[]{wsContext, restContext, new DefaultHandler()});
server.setHandler(handlers);
Help...
Update (additional information per Joakim Erdfelt request):
I am at class HTTPConnection class, in BP at onComplete() method, fetching the request headers from the _channel._fields object I get:
The response object's status is 200 (and not 101 as expected):
My endpoint object is part of a large inheritance chain. It is full of boilerplate business logic code that I need to remove before I can upload it, but in the root stands the javax.websocket.Endpont class, where we implemented only the onOpen(Session session, EndpointConfig config) method. I am not getting to that method when debugging, seems to fail long before...
Your request headers looks like this ...
Accept: application/json, application/*+json
Accept-Encoding: gzip
Cache-Control: no-cache
Connection: keep-alive
Content-Type: application/json
Host: 127.0.0.1:8080
Pragma: no-cache
Sec-WebSocket-Key: sMQPm6Cf00itLII3QBb4w==
Sec-WebSocket-Version: 13
Upgrade: websocket
User-Agent: Java/1.8.0_144
That is an invalid WebSocket Upgrade Request.
The most glaring omission is
Connection: upgrade
But there's also other fields that a compliant WebSocket Client would never set.
Content-Type: application/json
Accept: application/json, application/*+json
Accept-Encoding: gzip

MQTT over Websocket request / x-amzn-ErrorType: ForbiddenException

I am using ESP8266-Websocket, aws-sdk-arduino(cleaned branch) and pubsubclient to try to comunicate with aws iot mqtt service using websockets.
My question is about the first connection request. I am using this browser app as reference https://github.com/awslabs/aws-iot-examples and the sign code from aws-sdk-arduino (that works fine calling the aws iot restful api)
My request was this (after connect to the endpoint at 443 port):
GET wss://ENDPOINT.iot.us-west-2.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AWSKEY%2F20160318%2Fus-west-2%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=20160318T183246Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=a1f0d7b58983f9dff7e3bf6cab062db3243ebafc990803a018c6a23433891404 HTTP/1.1
host: ENDPOINT.iot.us-west-2.amazonaws.com
Connection: Upgrade
Upgrade: websocket
Origin: file://
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: D2alJOyUkBlR+8yhv2UBLg==
Sec-WebSocket-Protocol: mqtt
but I keep getting
HTTP/1.1 403 Forbidden
content-type: application/json
content-length: 241
date: Fri, 18 Mar 2016 18:34:57 GMT
x-amzn-RequestId: f2edfe83-1bbc-4481-97e0-39ccfc4d1c2f
connection: Keep-Alive
x-amzn-ErrorType: ForbiddenException:
am i missing some request header parameter? is there anyway to get a better feedback from x-amzn-ErrorType: ForbiddenException? am i messing up in the sign process? (even though it works for rest call)
Yeah, I've finally got response status 101 switching protocols \o/
when you are building the request, it must be:
GET /mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AWSKEY%2F20160318%2Fus-west-2%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=20160318T183246Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=a1f0d7b58983f9dff7e3bf6cab062db3243ebafc990803a018c6a23433891404 HTTP/1.1
instead of
GET wss://ENDPOINT.iot.us-west-2.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AWSKEY%2F20160318%2Fus-west-2%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=20160318T183246Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=a1f0d7b58983f9dff7e3bf6cab062db3243ebafc990803a018c6a23433891404 HTTP/1.1
the js example that I was following was using the full path. When I got the request built by chrome (throught developer tools) the path was full as well. Just after use firebug I got the real request that browser was sending to the server (with short path).
now I can continue to try the solution (mqtt over websockets at esp8266) :-) if it works, I will share the code ;-)

Mandrill API error for send request

I have a problem while sending a message via Jersey client on Mandrill API. I use Jersey client as follows:
ClientBuilder.newClient()
.register(JacksonJsonProvider.class)
.target(“https://mandrillapp.com/api/1.0/messages/send.json”)
.request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.json(methodEntity));
Below you can see logged headers, method and content of the API request.
POST https://mandrillapp.com/api/1.0/messages/send.json
Accept: application/json
Content-Type: application/json
{"message":{"subject":"Hello World!","text":"Really, Im just saying hi from Mandrill!","to":[{"email":"marcel#xxxx.com","name":"Marcel cccc","type":"to"}],"headers":{},"tags":["test"],"from_email":"info#xxxxx.com","auto_text":true,"preserve_recipients":false},"async":false,"key":"EWIBVEIOVBVOIEBWIOVEB"}
In response to this request I keep receiving following message:
[{"email":"marcel#XXXX.com","status":"rejected","_id":"0ea5e40fc2f3413ba85b765acdc5f17a","reject_reason":"invalid-sender"}]
I do not know what the issue may be, from some posts I figured out I must use UTF-8 to encode my message and headers. But setting encoding to UTF-8 did not do much good. Otherwise the payload seems fine to me and moreover I found on forums that invalid sender can mean any other kind of issue (not just invalid sender which is sad).
I had exactly same problem with
"reject_reason":"invalid-sender"
You probably check already similar question Mandrill “reject_reason”: “invalid-sender”
Try it if it helps. I realize that you also missing header parameter in your request
e.g. User-Agent: Mandrill-myclient/1.0
Please try also add this parameter to your Jersey Client setup as following:
ClientBuilder.newClient()
.register(JacksonJsonProvider.class)
.target(“https://mandrillapp.com/api/1.0/messages/send.json”)
.request(MediaType.APPLICATION_JSON_TYPE)
.header("User-Agent", "Mandrill-myclient/1.0")
.post(Entity.json(methodEntity));
Does it help?

Problem with webclient: Expectation failed?

I have a custom Http Handler which manipulates HTTP POST and GET. I got the project working on a seperate isolated server now need to put it in production...
using (var client = new WebClient())
{
client.Credentials = CredentialCache.DefaultCredentials;
client.UploadFile("serverlocation:port", fileToUpload);
}
For some reason now when using client.UploadFile("", file); i.e. forcing the HTTP POST
System.Net.WebException: The remote server returned an error: (417) Expectation failed.
at System.Net.WebClient.UploadFile(Uri address, String method, String fileName)
What could this be? I know the code works, so what else? Maybe the server blocks HTTP POST requests?
I have tried adding:
ServicePointManager.Expect100Continue = false;
But have had no success though i'm not 100% sure where this code should before, I assume before i'm using the WebClient
Edit 0 :
I have just read the following:
Because of the presence of older implementations, the protocol allows
ambiguous situations in which a client may send "Expect: 100-
continue" without receiving either a 417 (Expectation Failed) status
or a 100 (Continue) status. Therefore, when a client sends this
header field to an origin server (possibly via a proxy) from which it
has never seen a 100 (Continue) status, the client SHOULD NOT wait
for an indefinite period before sending the request body.
I believe this request is going through a proxy, which may have something to do with the issue.
Edit 1:
Believe this problem has to be with 100-continue because, using fiddler to see exactly what my application is sending with WebClient.UploadFile shows this:
POST http://XXX.XXX.XXX.XXX:8091/file.myhandledextension HTTP/1.1
Content-Type: multipart/form-data; boundary=---------------------8ccd1eb03f78bc2
Host: XXX.XXX.XXX.XXX:8091
Content-Length: 4492
Expect: 100-continue
Despite having put that line: ServicePointManager.Expect100Continue = false; before the using statement. I don't think this line actually works.
I ended up solving this by putting the ServicePointManager.Expect100Continue = false; in the constructor for the calling WebClient class.
Then I used Fiddler to examine the POST request to ensure Expect: 100-continue was not in the request anymore.

Resources