Skip to content

fail if user tries writing bytes after request is sent #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

artemredkin
Copy link
Collaborator

Fail if user tries writing body parts after request is sent.

Motivation:
This is could be a security issue.

Modifications:

  1. Check state when writing body parts and fail if its not idle
  2. Added a test

Result:
Closes #252

@artemredkin artemredkin added this to the 1.2.0 milestone Jun 23, 2020
@artemredkin artemredkin requested review from weissi and Lukasa June 23, 2020 14:48
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice patch

weissi
weissi previously requested changes Jun 23, 2020
Copy link
Contributor

@weissi weissi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! Left a few comments because I think some details aren't quite right yet.

@@ -424,11 +424,10 @@ class HTTPClientInternalTests: XCTestCase {
let randoEL = group.next()

let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group))
let promise: EventLoopPromise<Channel> = httpClient.eventLoopGroup.next().makePromise()
let httpBin = HTTPBin(channelPromise: promise)
let server = NIOHTTP1TestServer(group: MultiThreadedEventLoopGroup(numberOfThreads: 1))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't we leaking the group here? It needs to be stopped.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, we do

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

context.writeAndFlush(self.wrapOutboundOut(.body(part)), promise: promise)
default:
let error = HTTPClientError.writeAfterRequestSent
promise.fail(error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be quite right (and I think we should have a test for this). We're calling out to the user here before we change our state. So we may get user code "sandwiched in" here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've re-ordered things, not sure yet how to test it though, I'll have to get access to a handlers internal state... Could you elaborate on "sandwiched" code a bit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artemredkin sure. If you do promise.fail(...), then the promise's whenFailure and other code will run inline here. So we detected an error (ie. we should be in state .endOrError) but we call out before we entered the .endOrError state. So if the user for example uses another method that does a state check, we potentially have an issue because their code runs before the self.state = .endOrError so they won't see it. That could lead us to make wrong decisions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, that is so obvious now :) I'll think about how to test this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa could you pick up from Johannes here? I've added a test to verify that if delegate is called here, state is changed. I haven't figured out how to attach anything to that promise though... It seems that I need to have access to channel.write(HTTPClient.request, promise), and I don't think anything in the code can attach to it

private func writeBodyPart(context: ChannelHandlerContext, part: IOData, promise: EventLoopPromise<Void>) {
switch self.state {
case .idle:
self.actualBodyLength += part.readableBytes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think (would need a test here too) that we're policing the body length too late here, no? We seem to just add the readableBytes but I can't see that we check that it's actually less than or equal the number of body bytes allowed.

What happens if we do say content-length: 1 and then send XABCDEFG, aren't we then still sending ABCDEFG? I think we are and that'd be a security vulnerability because we could smuggle a request (say we did XGET /something HTTP/1.1\r\n\r\n

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thats a great point, yeah, we only check in the end, let me see how I can handle it here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I added an early fail here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artemredkin this looks good but given that it's security relevant, could we add a test specifically for that case (one that'd fail without the new condition)

Copy link
Collaborator Author

@artemredkin artemredkin Jun 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have it, by accident, we have two tests for content-length/body-length checks:

  • testContentLengthTooLongFails
  • testContentLengthTooShortFails
    Last one will fail at the end of the request, before we send .end, but the first one will now be failed by this new check

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially we can split the error message, just to be sure

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artemredkin I think we need a new one that tests that we do not send the bytes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because as soon as the bytes are on the wire, we created a security vulnerability. Even if we detect later that something's wrong, it'll be too late.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lukasa same here, I've added a test to check that we do not send more bytes then body length, although I'm not 100% sure its foolproof

@artemredkin artemredkin requested a review from Lukasa July 17, 2020 14:05
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One note in the test.

Regarding the promise that Johannes was talking about, you cannot attach to it directly so it does not immediately matter. In practice it will trigger the callouts to the delegate first: if those callouts are safe, the rest will be too.

}

XCTAssertThrowsError(try channel.writeOutbound(request))
XCTAssertTrue(delegate.triggered)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also check on the EmbeddedChannel that only the bytes we expected got written (i.e. the HTTP request head and nothing else, indicating that the check fired before the write did.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great idea, done!

@artemredkin artemredkin requested a review from Lukasa July 22, 2020 15:30
@Lukasa Lukasa requested a review from weissi July 24, 2020 13:01
@Lukasa
Copy link
Collaborator

Lukasa commented Jul 24, 2020

Sadly I don't have admin on this repo, so we'll need @tomerd to dismiss @weissi's out-of-date review.

@tomerd tomerd dismissed weissi’s stale review July 29, 2020 19:57

later review by @Lukasa

@tomerd
Copy link
Contributor

tomerd commented Jul 29, 2020

@Lukasa go ahead

@tomerd
Copy link
Contributor

tomerd commented Jul 29, 2020

cc @ktoso

@artemredkin artemredkin merged commit f69b68f into swift-server:master Jul 30, 2020
@artemredkin artemredkin deleted the fail_on_uploading_after_max branch July 30, 2020 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

uploading more bytes into a body after having said the body is fully uploaded doesn't fail
4 participants