fix: close request body when BasicAuth rejects a request - #5
Open
jpka wants to merge 2 commits into
Open
Conversation
When the middleware aborts an unauthenticated request, c.Request.Body was left open, so whatever backs it stayed held until the request was torn down. Close it on the rejection path, before aborting. closeRequestBody guards a nil context, a nil Request and a nil or NoBody Body, and drops the error from Close: the request is already being rejected, so failing to drain a body being discarded changes nothing. The success path is untouched, so downstream handlers still read the body. No manual drain in front of the Close. net/http sets doEarlyClose on server request bodies, so Body.Close already drains up to 256 KiB looking for EOF and leaves the connection reusable, and flags the body for the server to close the connection when more than that is pending. Close is idempotent, so the server's own Close after the handler returns is a no-op. This also makes the package build. It had no go.mod, auth.go referenced gin types that were not defined anywhere in the repo, auth_test.go called processAccounts and authorizationHeader which did not exist, base64 was imported but unused, and auth.go (package gin) shared a directory with main.go (package main). The middleware now builds against gin itself as a drop-in replacement for gin.BasicAuth, with the starter program moved to cmd/demo. Tests cover the rejection path (body closed exactly once, for wrong passwords, unknown users, missing and malformed headers), the success path (body left open and readable), nil-safety, a failing Close, Keep-Alive reuse under and over the drain limit, and concurrency. The body-close tests fail with the fix reverted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LehYDiBQajTGivEPN7umCT
This branch targets madalynerlge2/gin, so the module is named for that repository rather than the fork hosting the branch. Updates the import in cmd/demo and the README example to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LehYDiBQajTGivEPN7umCT
Closed
4 tasks
|
👀 We've notified the reward creators here. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/claim #1
Fixes the request-body leak described in issue #1: when the Basic auth middleware aborts an unauthenticated request,
c.Request.Bodywas left open, so whatever backs it stayed held until the request was torn down.The fix
Close the body on the rejection path, before aborting:
closeRequestBodyguards a nil context, a nilRequest, and a nil orNoBodyBody— a panic inside auth middleware would be a far worse outcome than a body left open. It drops the error fromClose: the request is already being rejected, so failing to drain a body being discarded changes nothing. The success path is untouched, so downstream handlers still read the body normally.No manual drain before the Close
The previous attempt at this fix called
io.CopyN(io.Discard, body, 4096)before closing. That's removed.net/httpsetsdoEarlyCloseon server request bodies (server.go, inconn.readRequest), which already makesBody.Closedrain up tomaxPostHandlerReadBytes(256 KiB) looking for EOF and leave the connection reusable — and flag the body so the server closes the connection when more than that is still pending, rather than reusing a desynchronised one. The hand-rolled 4096-byte drain duplicated that logic less correctly.Closeis also idempotent, so the server's ownCloseafter the handler returns is a no-op.Why the diff is larger than the fix
Nothing in the repository compiled before this change:
go.modat allauth.goreferencedContextandHandlerFunc, defined nowhere in the repoauth_test.gocalledprocessAccountsandauthorizationHeader, which did not existencoding/base64was imported but unusedauth.go(package gin) shared a directory withmain.go(package main)So the middleware now builds against gin itself as a drop-in replacement for
gin.BasicAuth—BasicAuth,BasicAuthForRealm,AccountsandAuthUserKeymirror gin's API, so an existing call site swaps over unchanged. The starter program moved tocmd/demo, which serves as a manual check.Tests
go test -race ./...passes;gofmtandgo vetare clean.Request, nilBody,http.NoBodyClose— still yields a clean 401, no panichttptracethat three consecutive rejected requests carrying bodies reuse one connection ([false, true, true]), plus a 1 MiB over-the-drain-limit case-raceReverting just the
closeRequestBodycall makes the four body-close tests fail, so they genuinely bind to the fix. The Keep-Alive tests pass either way by design — they are no-regression guards.One note on the premise
Worth being straight about scope. Under a plain
net/httpserver the leak is bounded: the server closes the request body itself once the handler returns, so an unclosed body is held for the rest of that request rather than indefinitely, and theCLOSE_WAITgrowth described in the issue is more likely to come from clients or proxies that never read the 401 response. Closing early is still worth doing — it shortens the hold time under load, and it is the only thing that releases bodies wrapping a resource of their own (a decompressor, a temp file, a metered reader). The README documents this rather than overstating the impact.