[Fix] safeFetch streaming maxSize memory DoS regression test (#408)#534
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a regression test for safeFetch to ensure that it properly handles oversized streamed responses. By validating that the fetch operation terminates and cancels the stream reader as soon as the size limit is reached, it confirms the fix for a potential memory denial-of-service vulnerability. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
Hi @advancedresearcharray, DetailsInstructions for interacting with me using comments are available here. |
clawwork-ai#408) The streaming maxSize enforcement and reader cancellation were merged in clawwork-ai#487 and clawwork-ai#523, but issue clawwork-ai#408 remained open. Add a regression test that verifies safeFetch stops reading an oversized body before draining all chunks, preventing the memory DoS described in the issue. Closes clawwork-ai#408 Signed-off-by: Array Agent <advancedresearcharray@array.com> Co-authored-by: Cursor <cursoragent@cursor.com>
de3b747 to
34b9ebb
Compare
There was a problem hiding this comment.
Code Review
This pull request adds a new test case to verify that safeFetch implements bounded streaming and does not read an entire oversized body before rejecting. The review feedback suggests simplifying the test mock setup by directly spying on the reader returned by streamReaderFrom and returning it, rather than manually reconstructing a duplicate reader object.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const reader = streamReaderFrom(new ArrayBuffer(bodySize), chunkSize, cancel); | ||
| const read = vi.fn(reader.read.bind(reader)); | ||
| netFetchMock.mockResolvedValue({ | ||
| ok: true, | ||
| status: 200, | ||
| headers: new Headers(), | ||
| body: { | ||
| getReader: () => | ||
| ({ | ||
| read, | ||
| cancel, | ||
| releaseLock: () => {}, | ||
| closed: Promise.resolve(undefined), | ||
| }) as ReadableStreamDefaultReader<Uint8Array>, | ||
| }, | ||
| } as unknown as Response); | ||
|
|
||
| await expect(safeFetch('https://cdn.example.com/huge-stream.bin', { maxSize })).rejects.toThrow('too large'); | ||
|
|
||
| // 5 chunks (1280 bytes) exceed maxSize; must not drain the full 10 KiB body. | ||
| expect(read.mock.calls.length).toBeLessThan(bodySize / chunkSize); | ||
| expect(read.mock.calls.length).toBeLessThanOrEqual(Math.ceil(maxSize / chunkSize) + 1); | ||
| expect(cancel).toHaveBeenCalledOnce(); |
There was a problem hiding this comment.
The streamReaderFrom helper function already returns a fully-formed ReadableStreamDefaultReader<Uint8Array> object containing read, cancel, releaseLock, and closed properties. Instead of manually reconstructing a duplicate reader object in the getReader mock and manually binding/wrapping reader.read with vi.fn, you can simplify the test by spying on the reader directly with vi.spyOn(reader, 'read') and returning reader from getReader.
| const reader = streamReaderFrom(new ArrayBuffer(bodySize), chunkSize, cancel); | |
| const read = vi.fn(reader.read.bind(reader)); | |
| netFetchMock.mockResolvedValue({ | |
| ok: true, | |
| status: 200, | |
| headers: new Headers(), | |
| body: { | |
| getReader: () => | |
| ({ | |
| read, | |
| cancel, | |
| releaseLock: () => {}, | |
| closed: Promise.resolve(undefined), | |
| }) as ReadableStreamDefaultReader<Uint8Array>, | |
| }, | |
| } as unknown as Response); | |
| await expect(safeFetch('https://cdn.example.com/huge-stream.bin', { maxSize })).rejects.toThrow('too large'); | |
| // 5 chunks (1280 bytes) exceed maxSize; must not drain the full 10 KiB body. | |
| expect(read.mock.calls.length).toBeLessThan(bodySize / chunkSize); | |
| expect(read.mock.calls.length).toBeLessThanOrEqual(Math.ceil(maxSize / chunkSize) + 1); | |
| expect(cancel).toHaveBeenCalledOnce(); | |
| const reader = streamReaderFrom(new ArrayBuffer(bodySize), chunkSize, cancel); | |
| const readSpy = vi.spyOn(reader, 'read'); | |
| netFetchMock.mockResolvedValue({ | |
| ok: true, | |
| status: 200, | |
| headers: new Headers(), | |
| body: { | |
| getReader: () => reader, | |
| }, | |
| } as unknown as Response); | |
| await expect(safeFetch('https://cdn.example.com/huge-stream.bin', { maxSize })).rejects.toThrow('too large'); | |
| // 5 chunks (1280 bytes) exceed maxSize; must not drain the full 10 KiB body. | |
| expect(readSpy.mock.calls.length).toBeLessThan(bodySize / chunkSize); | |
| expect(readSpy.mock.calls.length).toBeLessThanOrEqual(Math.ceil(maxSize / chunkSize) + 1); | |
| expect(cancel).toHaveBeenCalledOnce(); |
…lawwork-ai#408) Use vi.spyOn on streamReaderFrom's read method instead of reconstructing a duplicate reader object, addressing PR review feedback on clawwork-ai#534. Signed-off-by: Array Agent <advancedresearcharray@array.com> Co-authored-by: Cursor <cursoragent@cursor.com>
|
/auto-cc |
|
Friendly follow-up: CI is green on the latest commit and this PR is ready for review/merge when you have a moment. Thanks! |
|
Thanks @advancedresearcharray! |
Closes clawwork-ai#408 The streaming maxSize enforcement (clawwork-ai#487), reader cancellation (clawwork-ai#523), and regression tests (clawwork-ai#534) are already on main. This commit closes the issue that remained open after squash merges omitted the closing keyword. Signed-off-by: advancedresearcharray <advancedresearcharray@github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Closes clawwork-ai#408 The streaming maxSize enforcement (clawwork-ai#487), reader cancellation (clawwork-ai#523), and regression tests (clawwork-ai#534) are already on main. This commit closes the issue that remained open after squash merges omitted the closing keyword. Signed-off-by: advancedresearcharray <advancedresearcharray@github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Closes clawwork-ai#408 The streaming maxSize enforcement (clawwork-ai#487), reader cancellation (clawwork-ai#523), and regression tests (clawwork-ai#534) are already on main. This commit closes the issue that remained open after squash merges omitted the closing keyword. Signed-off-by: advancedresearcharray <advancedresearcharray@github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Verified reader.cancel() + controller.abort() on streamed bytes exceeding maxSize, IPv6 pinned hostname bracketing, and 25 regression tests passing. Fix already landed on main via clawwork-ai#487, clawwork-ai#523, clawwork-ai#534; this commit closes clawwork-ai#408. Closes clawwork-ai#408 Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
safeFetchbuffered the entire response viares.arrayBuffer()before enforcingmaxSize, enabling a memory DoS. The streaming reader fix landed in fix(net): stream safeFetch body with incremental maxSize enforcement #487 and reader cancellation in [Fix] Cancel safeFetch body reader when maxSize exceeded #523, but the issue remained open.safeFetchstops reading an oversized streamed body before draining all chunks (bounded reads + reader cancel), matching the memory-DoS scenario in the issue.Closes #408
Test plan
pnpm --filter @clawwork/desktop test -- safe-fetch.test.ts— 25 tests passeddoes not read the entire oversized body before rejecting — bounded streaming (#408)release-note
NONE