Skip to content

Commit 93161e8

Browse files
authored
PLU-556: GZip compression bombs (#1198)
## Addresses VAPT issue * Streams the http response and throws an error if it exceeds the max content length or max compression ratio * Set timeout to prevent hanging requests
1 parent f299103 commit 93161e8

File tree

8 files changed

+497
-0
lines changed

8 files changed

+497
-0
lines changed

packages/backend/src/apps/custom-api/__tests__/actions/http-request.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({
2020
stepQueryResult: vi.fn(() => ({
2121
config: {},
2222
})),
23+
addInterceptors: vi.fn(),
2324
}))
2425

2526
vi.mock('../../common/ip-resolver', () => {
@@ -30,6 +31,10 @@ vi.mock('../../common/ip-resolver', () => {
3031
}
3132
})
3233

34+
vi.mock('../../common/add-interceptors', () => ({
35+
default: mocks.addInterceptors,
36+
}))
37+
3338
vi.mock('@/models/step', () => ({
3439
default: {
3540
query: () => ({
@@ -78,6 +83,7 @@ describe('make http request', () => {
7883
url: $.step.parameters.url,
7984
method: $.step.parameters.method,
8085
data: $.step.parameters.data,
86+
responseType: 'stream',
8187
}),
8288
)
8389
})
@@ -98,6 +104,7 @@ describe('make http request', () => {
98104
url: $.step.parameters.url,
99105
method: $.step.parameters.method,
100106
data: $.step.parameters.data,
107+
responseType: 'stream',
101108
headers: {
102109
Key1: 'Value1',
103110
Key2: 'Value2',
@@ -174,13 +181,15 @@ describe('make http request', () => {
174181
url: 'http://test.local/endpoint?1234',
175182
method: 'POST',
176183
data: 'meep meep',
184+
responseType: 'stream',
177185
}),
178186
)
179187
expect(mocks.httpRequest).toHaveBeenCalledWith(
180188
expect.objectContaining({
181189
url: 'https://redirect.com',
182190
method: 'GET',
183191
data: 'meep meep',
192+
responseType: 'stream',
184193
}),
185194
)
186195
})
@@ -203,13 +212,15 @@ describe('make http request', () => {
203212
url: 'http://test.local/endpoint?1234',
204213
method: 'POST',
205214
data: 'meep meep',
215+
responseType: 'stream',
206216
}),
207217
)
208218
expect(mocks.httpRequest).toHaveBeenCalledWith(
209219
expect.objectContaining({
210220
url: 'https://redirect.com',
211221
method: 'POST',
212222
data: 'meep meep',
223+
responseType: 'stream',
213224
}),
214225
)
215226
})
@@ -254,6 +265,7 @@ describe('make http request', () => {
254265
expect(mocks.httpRequest).toHaveBeenCalledWith(
255266
expect.objectContaining({
256267
timeout: CUSTOM_API_TIMEOUT,
268+
responseType: 'stream',
257269
}),
258270
)
259271
})
@@ -277,6 +289,7 @@ describe('make http request', () => {
277289
expect(mocks.httpRequest).toHaveBeenCalledWith(
278290
expect.objectContaining({
279291
timeout: 360000,
292+
responseType: 'stream',
280293
}),
281294
)
282295
})
@@ -301,6 +314,7 @@ describe('make http request', () => {
301314
expect(mocks.httpRequest).toHaveBeenCalledWith(
302315
expect.objectContaining({
303316
timeout: CUSTOM_API_TIMEOUT,
317+
responseType: 'stream',
304318
}),
305319
)
306320
},
@@ -353,6 +367,7 @@ describe('make http request', () => {
353367
url: $.step.parameters.url,
354368
method: $.step.parameters.method,
355369
data: $.step.parameters.data as any,
370+
responseType: 'stream',
356371
}),
357372
)
358373
},
@@ -381,6 +396,7 @@ describe('make http request', () => {
381396
url: $.step.parameters.url,
382397
method: $.step.parameters.method,
383398
data: $.step.parameters.data as any,
399+
responseType: 'stream',
384400
}),
385401
)
386402
},
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Readable, Transform, Writable } from 'stream'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { createSizeMonitor } from '@/apps/custom-api/common/size-monitor'
5+
6+
const mocks = vi.hoisted(() => ({
7+
warn: vi.fn(),
8+
}))
9+
10+
vi.mock('@/helpers/logger', () => ({
11+
default: {
12+
warn: mocks.warn,
13+
},
14+
}))
15+
16+
// Helper to stream buffers through a Transform and collect output
17+
const writeBuffers = (monitor: Transform, buffers: Buffer[]) =>
18+
new Promise<Buffer>((resolve, reject) => {
19+
const chunks: Buffer[] = []
20+
21+
const source = Readable.from(buffers)
22+
const sink = new Writable({
23+
write(chunk, _enc, cb) {
24+
chunks.push(Buffer.from(chunk))
25+
cb()
26+
},
27+
})
28+
29+
source.on('error', reject)
30+
monitor.on('error', reject)
31+
sink.on('error', reject)
32+
sink.on('finish', () => resolve(Buffer.concat(chunks)))
33+
34+
source.pipe(monitor).pipe(sink)
35+
})
36+
37+
describe('createSizeMonitor', () => {
38+
beforeEach(() => {
39+
vi.clearAllMocks()
40+
})
41+
afterEach(() => {
42+
vi.restoreAllMocks()
43+
})
44+
45+
it('passes through data under the maximum size', async () => {
46+
const monitor = createSizeMonitor()
47+
const input = [Buffer.alloc(1024), Buffer.from('hello')]
48+
const output = await writeBuffers(monitor, input)
49+
expect(output.length).toBe(input[0].length + input[1].length)
50+
expect(mocks.warn).not.toHaveBeenCalled()
51+
})
52+
53+
it('errors when total size exceeds 20MB', async () => {
54+
const monitor = createSizeMonitor()
55+
const overLimit = Buffer.alloc(20 * 1024 * 1024 + 1)
56+
await expect(writeBuffers(monitor, [overLimit])).rejects.toMatchObject({
57+
name: 'AxiosError',
58+
isAxiosError: true,
59+
response: {
60+
status: 413,
61+
statusText: 'Payload Too Large',
62+
data: { error: 'Response body too large' },
63+
},
64+
})
65+
})
66+
67+
it('errors when compression ratio exceeds the limit and warns', async () => {
68+
const monitor = createSizeMonitor(1) // compressed size 1 byte
69+
await expect(
70+
writeBuffers(monitor, [Buffer.alloc(200)]),
71+
).rejects.toMatchObject({
72+
name: 'AxiosError',
73+
isAxiosError: true,
74+
response: expect.objectContaining({ status: 413 }),
75+
})
76+
expect(mocks.warn).toHaveBeenCalled()
77+
})
78+
})

0 commit comments

Comments
 (0)