Repository navigation
fix: cap HTTP response body reads to prevent heap exhaustion - #382
Merged
Merged
Conversation
Requestor read entire response bodies into memory with no size limit, so a compromised upstream, misbehaving proxy, or MITM returning a very large body could exhaust the JVM heap. Response bodies are now capped at 10 MB. A Content-Length above the cap is rejected before anything is read, and the cap is also enforced while streaming in case the header is missing or wrong. Oversized responses throw an HttpError with a descriptive message. The App Engine fallback path applies the same cap. SEC-787 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 of 4 tasks
Justin Hammond (Justintime50)
approved these changes
Oct 5, 2026
Byte Buddy bundled with Mockito 4.11 rejects Java 25 class files, breaking RequestorTest (mocks HttpsURLConnection). Set net.bytebuddy.experimental for surefire. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Matt Sutkus (msuitcase)
added a commit
that referenced
this pull request
Oct 21, 2026
# Description Fixes SEC-786 (pentest finding "API Key Forwarded to Request-Hook Handlers", Medium). `Requestor.httpRequest` built the hook headers with `generateHeaders(client.getApiKey())` and passed them to both `RequestHookResponses` and `ResponseHookResponses`, so every hook received `Authorization: Bearer <full api key>`. A hook that logs or exports its event (a common use for debugging, tracing, or APM) would write the full key to logs or a third-party platform. Changes: - **Hooks get a redacted copy of the headers.** The new `generateHookHeaders` builds the hook headers map and replaces the `Authorization` value with `Bearer ****WXYZ` (last four characters only). This keeps enough auth context to tell keys apart in logs without exposing the credential. - The mask has a fixed length, so it doesn't reveal the key length either. - Keys of 8 characters or fewer are fully masked (`Bearer ****`), since showing four characters would reveal half or more of the key. Real EasyPost keys are much longer, so this only affects unusual or test keys. - **The outbound request is unchanged.** `createEasyPostConnection` and the App Engine path still build their own headers with the full key. The hook map was already a separate instance from the one used on the connection; it's now also redacted. - Updated the `headers` Javadoc on both hook classes. The `ResponseHookResponses` doc said "headers of the response", but these are the request headers. - CHANGELOG entry under `Next Release`. `Authorization` is the only sensitive header the SDK sets, so no other headers needed redacting. # Testing New tests in `HookTest` send a request through a mocked `HttpsURLConnection` (via `EasyPost._vcrUrlFunction`, the same approach as #382) and capture what both hooks receive: - `testHooksReceiveRedactedApiKey`: both hooks see `Bearer ****WXYZ`, no header value contains the full key, and `Mockito.verify` confirms the real connection was sent `Bearer <full key>`. - `testHooksFullyRedactShortApiKey`: an 8-character key is fully masked in both hooks and still sent in full on the wire. The existing VCR hook tests are unchanged. An `@AfterEach` resets `_vcrUrlFunction` so the mock doesn't leak into other tests.⚠️ I couldn't build this locally (my sandbox couldn't clone the repo or reach Maven), so CI is the first compile, checkstyle and test run. **Notes for merging:** - #382 also adds a line under `Next Release` in `CHANGELOG.md`, so whichever lands second will have a one-line conflict to resolve. The code changes touch different parts of `Requestor` and don't overlap. - Same release caveat as #382: the ticket asks for a patch release, but `Next Release` already contains a breaking change (`addCreditCardToUser` removal). A true 8.8.1 would need a backport branch from `v8.8.0`; this change applies there cleanly in concept (only `httpRequest` and two new private helpers). - The ticket also asks to check the other SDKs (node, python, ruby, php, go, csharp) for the same hook behavior. That's not covered here. # Pull Request Type Please select the option(s) that are relevant to this PR. - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Improvement (fixing a typo, updating readme, renaming a variable name, etc) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Description
Fixes SEC-787 (pentest finding, Medium).
Requestorread whole HTTP response bodies into memory with no size limit (Scanner(...).useDelimiter("\\A")). A compromised upstream, a misbehaving proxy, or a MITM sending a very large body could exhaust the JVM heap.Changes:
getResponseBody(InputStream, long contentLength)reads in 8 KB chunks and stops atConstants.Http.MAX_RESPONSE_BODY_BYTES(10 MB).HttpErrorwith a descriptive message (Constants.ErrorMessages.RESPONSE_BODY_TOO_LARGE), not an OOM.HttpErroris not anIOException, so the generic "could not connect" wrapper doesn't swallow it.nullerror stream (a 4xx/5xx with no body) now returns""instead of throwing an NPE.available() == 0short-circuit, which could wrongly return""for a real stream that just had no bytes buffered yet.makeAppEngineRequest).ReferralCustomerService.createStripeToken. That code was already removed onmasterin chore: remove deprecated, unused addCreditCardToUser function #381 (addCreditCardToUserremoval), so nothing is left to patch here.No unbounded reads remain in
src/main.Release plan: no backport to 8.8.x. Both halves of SEC-787 ship together in the next release from
master: the Stripe-path removal from #381 and this cap. Users get both by upgrading to that version. Note that the release includes the breakingaddCreditCardToUserremoval, so its version number should be chosen accordingly.Testing
New
RequestorTest(it extendsRequestorto reach the protected helper, the same patternErrorTestuses):client.address.retrieve(...)with a mockedHttpsURLConnection(viaEasyPost._vcrUrlFunction): a normal response deserializes, and both oversized cases throwHttpErrorPull Request Type
Please select the option(s) that are relevant to this PR.
🤖 Generated with Claude Code