diff --git a/spec/CloudCodeMultipart.spec.js b/spec/CloudCodeMultipart.spec.js index b2f60c0761..943df7b264 100644 --- a/spec/CloudCodeMultipart.spec.js +++ b/spec/CloudCodeMultipart.spec.js @@ -363,4 +363,62 @@ describe('Cloud Code Multipart', () => { expect(result.status).toBe(200); expect(result.data.result.isMaster).toBe(false); }); + + it('should reject multipart request with many empty parts whose wire size exceeds maxUploadSize', async () => { + await reconfigureServer({ maxUploadSize: '1kb' }); + + Parse.Cloud.define('multipartManyEmptyParts', req => { + return { count: Object.keys(req.params).length }; + }); + + const boundary = '----TestBoundaryManyEmptyParts'; + const parts = []; + for (let i = 0; i < 2000; i++) { + parts.push({ name: `f${i}`, value: '' }); + } + const body = buildMultipartBody(boundary, parts); + // The wire body is far larger than maxUploadSize even though every field + // value is empty, so the value/chunk byte counters alone never trip. + expect(body.length).toBeGreaterThan(100 * 1024); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartManyEmptyParts`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE); + }); + + it('should reject multipart request whose Content-Length exceeds maxUploadSize', async () => { + await reconfigureServer({ maxUploadSize: '1kb' }); + + Parse.Cloud.define('multipartContentLength', req => { + return { count: Object.keys(req.params).length }; + }); + + const boundary = '----TestBoundaryContentLength'; + const parts = []; + for (let i = 0; i < 2000; i++) { + parts.push({ name: `f${i}`, value: '' }); + } + const body = buildMultipartBody(boundary, parts); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartContentLength`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'Content-Length': String(body.length), + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE); + }); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 8bcf8a5858..61b3545333 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -201,6 +201,16 @@ export class FunctionsRouter extends PromiseRouter { return Promise.resolve(); } const maxBytes = Utils.parseSizeToBytes(req.config.maxUploadSize); + // Reject early when the declared request size already exceeds the limit. + const contentLength = Number(req.headers['content-length']); + if (Number.isFinite(contentLength) && contentLength > maxBytes) { + return Promise.reject( + new Parse.Error( + Parse.Error.OBJECT_TOO_LARGE, + 'Multipart request exceeds maximum upload size.' + ) + ); + } return new Promise((resolve, reject) => { const fields = Object.create(null); let totalBytes = 0; @@ -213,11 +223,12 @@ export class FunctionsRouter extends PromiseRouter { new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`) ); } - const safeReject = (err) => { + const safeReject = err => { if (settled) { return; } settled = true; + req.unpipe(busboy); busboy.destroy(); reject(err); }; @@ -280,6 +291,23 @@ export class FunctionsRouter extends PromiseRouter { new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`) ); }); + // Enforce `maxUploadSize` against the raw request bytes (multipart + // boundaries, part headers, field names and part count included), not only + // the parsed field values and file contents. This mirrors how + // `express.json` bounds non-multipart bodies and stops a request composed + // of many empty parts from exceeding the limit on the wire. + let rawBytes = 0; + req.on('data', chunk => { + rawBytes += chunk.length; + if (rawBytes > maxBytes) { + safeReject( + new Parse.Error( + Parse.Error.OBJECT_TOO_LARGE, + 'Multipart request exceeds maximum upload size.' + ) + ); + } + }); req.pipe(busboy); }); }