Skip to content

Commit 0c089b7

Browse files
authored
fix(server): reject non-GET sub-requests in GET batch requests (#1813)
GET batch requests now execute only sub-requests that explicitly use the GET method. A hand-crafted `data` query parameter could previously smuggle POST sub-requests through a plain GET request, executing mutations past defenses that treat GET as safe (CSRF protections, caches, method-based rules). ## Fixes - `BatchHandlerPlugin` rejects a GET batch with `400 GET batch requests only accept GET sub-requests` before any sub-request executes if any request message carries a non-GET method. - A sub-request with the method omitted defaults to POST at execution time, so it is rejected too; only an explicit `GET` passes. - Legitimate clients are unaffected: `BatchLinkPlugin` only places GET calls into GET batches and always serializes their method explicitly. ## Testing - New tests cover a POST sub-request and an omitted-method sub-request inside a GET batch; both return 400 and the handler never runs. - All batch plugin, client, and e2e batch tests pass; lint and type checks are clean.
1 parent 29a900b commit 0c089b7

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

packages/server/src/plugins/batch.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,41 @@ describe('batchHandlerPlugin', () => {
307307
expect(response!.status).toBe(400)
308308
expect(await response!.text()).toContain('Invalid batch request data parameter')
309309
})
310+
311+
it('returns 400 when a GET batch contains a non-GET sub-request', async () => {
312+
const handler = createHandler()
313+
const data = encodeURIComponent(JSON.stringify([
314+
makePeerRequestMessage(0, '/ping', 'GET'),
315+
makePeerRequestMessage(1, '/ping', 'POST'),
316+
]))
317+
318+
const { response } = await handler.handle(createBatchRequest({
319+
mode: 'buffered',
320+
method: 'GET',
321+
data,
322+
}))
323+
324+
expect(response!.status).toBe(400)
325+
expect(await response!.text()).toContain('GET batch requests only accept GET sub-requests')
326+
expect(handlerFn).toHaveBeenCalledTimes(0)
327+
})
328+
329+
it('returns 400 when a GET batch sub-request omits the method (defaults to POST)', async () => {
330+
const handler = createHandler()
331+
const data = encodeURIComponent(JSON.stringify([
332+
{ kind: 'request', id: 0, json: { url: '/ping', headers: {} } },
333+
]))
334+
335+
const { response } = await handler.handle(createBatchRequest({
336+
mode: 'buffered',
337+
method: 'GET',
338+
data,
339+
}))
340+
341+
expect(response!.status).toBe(400)
342+
expect(await response!.text()).toContain('GET batch requests only accept GET sub-requests')
343+
expect(handlerFn).toHaveBeenCalledTimes(0)
344+
})
310345
})
311346

312347
describe('configuration options', () => {

packages/server/src/plugins/batch.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ export class BatchHandlerPlugin<T extends Context> implements StandardHandlerPlu
148148
}
149149
}
150150

151+
/**
152+
* A GET batch must not execute non-GET sub-requests, otherwise defenses
153+
* that treat GET as safe (CSRF protections, caches, method-based rules)
154+
* can be bypassed. An absent method defaults to POST, so require an explicit GET.
155+
*/
156+
if (mightBeMessages.some(m => m.kind === 'request' && m.json.method !== 'GET')) {
157+
return {
158+
matched: true,
159+
response: { status: 400, headers: {}, body: 'GET batch requests only accept GET sub-requests' },
160+
}
161+
}
162+
151163
messages = mightBeMessages
152164
}
153165
else {

0 commit comments

Comments
 (0)