Skip to content

Commit f12e1c3

Browse files
authored
fix: Cloud Function multipart requests bypass the maxUploadSize limit (#10498)
1 parent 78859a9 commit f12e1c3

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

spec/CloudCodeMultipart.spec.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,4 +363,62 @@ describe('Cloud Code Multipart', () => {
363363
expect(result.status).toBe(200);
364364
expect(result.data.result.isMaster).toBe(false);
365365
});
366+
367+
it('should reject multipart request with many empty parts whose wire size exceeds maxUploadSize', async () => {
368+
await reconfigureServer({ maxUploadSize: '1kb' });
369+
370+
Parse.Cloud.define('multipartManyEmptyParts', req => {
371+
return { count: Object.keys(req.params).length };
372+
});
373+
374+
const boundary = '----TestBoundaryManyEmptyParts';
375+
const parts = [];
376+
for (let i = 0; i < 2000; i++) {
377+
parts.push({ name: `f${i}`, value: '' });
378+
}
379+
const body = buildMultipartBody(boundary, parts);
380+
// The wire body is far larger than maxUploadSize even though every field
381+
// value is empty, so the value/chunk byte counters alone never trip.
382+
expect(body.length).toBeGreaterThan(100 * 1024);
383+
384+
const result = await postMultipart(
385+
`http://localhost:8378/1/functions/multipartManyEmptyParts`,
386+
{
387+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
388+
'X-Parse-Application-Id': 'test',
389+
'X-Parse-REST-API-Key': 'rest',
390+
},
391+
body
392+
);
393+
394+
expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE);
395+
});
396+
397+
it('should reject multipart request whose Content-Length exceeds maxUploadSize', async () => {
398+
await reconfigureServer({ maxUploadSize: '1kb' });
399+
400+
Parse.Cloud.define('multipartContentLength', req => {
401+
return { count: Object.keys(req.params).length };
402+
});
403+
404+
const boundary = '----TestBoundaryContentLength';
405+
const parts = [];
406+
for (let i = 0; i < 2000; i++) {
407+
parts.push({ name: `f${i}`, value: '' });
408+
}
409+
const body = buildMultipartBody(boundary, parts);
410+
411+
const result = await postMultipart(
412+
`http://localhost:8378/1/functions/multipartContentLength`,
413+
{
414+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
415+
'Content-Length': String(body.length),
416+
'X-Parse-Application-Id': 'test',
417+
'X-Parse-REST-API-Key': 'rest',
418+
},
419+
body
420+
);
421+
422+
expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE);
423+
});
366424
});

src/Routers/FunctionsRouter.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ export class FunctionsRouter extends PromiseRouter {
201201
return Promise.resolve();
202202
}
203203
const maxBytes = Utils.parseSizeToBytes(req.config.maxUploadSize);
204+
// Reject early when the declared request size already exceeds the limit.
205+
const contentLength = Number(req.headers['content-length']);
206+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
207+
return Promise.reject(
208+
new Parse.Error(
209+
Parse.Error.OBJECT_TOO_LARGE,
210+
'Multipart request exceeds maximum upload size.'
211+
)
212+
);
213+
}
204214
return new Promise((resolve, reject) => {
205215
const fields = Object.create(null);
206216
let totalBytes = 0;
@@ -213,11 +223,12 @@ export class FunctionsRouter extends PromiseRouter {
213223
new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`)
214224
);
215225
}
216-
const safeReject = (err) => {
226+
const safeReject = err => {
217227
if (settled) {
218228
return;
219229
}
220230
settled = true;
231+
req.unpipe(busboy);
221232
busboy.destroy();
222233
reject(err);
223234
};
@@ -280,6 +291,23 @@ export class FunctionsRouter extends PromiseRouter {
280291
new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`)
281292
);
282293
});
294+
// Enforce `maxUploadSize` against the raw request bytes (multipart
295+
// boundaries, part headers, field names and part count included), not only
296+
// the parsed field values and file contents. This mirrors how
297+
// `express.json` bounds non-multipart bodies and stops a request composed
298+
// of many empty parts from exceeding the limit on the wire.
299+
let rawBytes = 0;
300+
req.on('data', chunk => {
301+
rawBytes += chunk.length;
302+
if (rawBytes > maxBytes) {
303+
safeReject(
304+
new Parse.Error(
305+
Parse.Error.OBJECT_TOO_LARGE,
306+
'Multipart request exceeds maximum upload size.'
307+
)
308+
);
309+
}
310+
});
283311
req.pipe(busboy);
284312
});
285313
}

0 commit comments

Comments
 (0)