fix: Cloud Function multipart requests bypass the maxUploadSize limit (#10498)

This commit is contained in:
Manuel
2026-06-06 02:43:56 +02:00
committed by GitHub
parent 78859a9bc7
commit f12e1c3e31
2 changed files with 87 additions and 1 deletions
+58
View File
@@ -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);
});
});
+29 -1
View File
@@ -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);
});
}