From 169c6f1958b7efd6c94592b793733bac20404a2f Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:24:14 +0100 Subject: [PATCH 01/10] dep --- package-lock.json | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index b55c6a4595..0f95cc15ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "dependencies": { "@apollo/server": "5.5.0", "@as-integrations/express5": "1.1.2", + "@fastify/busboy": "3.2.0", "@graphql-tools/merge": "9.1.7", "@graphql-tools/schema": "10.0.31", "@graphql-tools/utils": "11.0.0", diff --git a/package.json b/package.json index a32752708d..080e388b9a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "dependencies": { "@apollo/server": "5.5.0", "@as-integrations/express5": "1.1.2", + "@fastify/busboy": "3.2.0", "@graphql-tools/merge": "9.1.7", "@graphql-tools/schema": "10.0.31", "@graphql-tools/utils": "11.0.0", From 0c4cc42438a1460735913ca12d49cdb7573afad9 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:32:30 +0100 Subject: [PATCH 02/10] feat --- spec/CloudCodeMultipart.spec.js | 284 ++++++++++++++++++++++++++++++++ src/ParseServer.ts | 2 +- src/Routers/FunctionsRouter.js | 80 +++++++++ 3 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 spec/CloudCodeMultipart.spec.js diff --git a/spec/CloudCodeMultipart.spec.js b/spec/CloudCodeMultipart.spec.js new file mode 100644 index 0000000000..16570ef6ee --- /dev/null +++ b/spec/CloudCodeMultipart.spec.js @@ -0,0 +1,284 @@ +'use strict'; +const http = require('http'); + +function postMultipart(url, headers, body) { + return new Promise((resolve, reject) => { + const parsed = new URL(url); + const req = http.request( + { + method: 'POST', + hostname: parsed.hostname, + port: parsed.port, + path: parsed.pathname, + headers, + }, + res => { + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => { + const raw = Buffer.concat(chunks).toString(); + try { + resolve({ status: res.statusCode, data: JSON.parse(raw) }); + } catch { + resolve({ status: res.statusCode, data: raw }); + } + }); + } + ); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +function buildMultipartBody(boundary, parts) { + const segments = []; + for (const part of parts) { + segments.push(`--${boundary}\r\n`); + if (part.filename) { + segments.push( + `Content-Disposition: form-data; name="${part.name}"; filename="${part.filename}"\r\n` + ); + segments.push(`Content-Type: ${part.contentType || 'application/octet-stream'}\r\n\r\n`); + segments.push(part.data); + } else { + segments.push(`Content-Disposition: form-data; name="${part.name}"\r\n\r\n`); + segments.push(part.value); + } + segments.push('\r\n'); + } + segments.push(`--${boundary}--\r\n`); + return Buffer.concat(segments.map(s => (typeof s === 'string' ? Buffer.from(s) : s))); +} + +describe('Cloud Code Multipart', () => { + it('should not reject multipart requests at the JSON parser level', async () => { + Parse.Cloud.define('multipartTest', req => { + return { received: true }; + }); + + const boundary = '----TestBoundary123'; + const body = buildMultipartBody(boundary, [ + { name: 'key', value: 'value' }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartTest`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).not.toBe(400); + }); + + it('should parse text fields from multipart request', async () => { + Parse.Cloud.define('multipartText', req => { + return { userId: req.params.userId, count: req.params.count }; + }); + + const boundary = '----TestBoundary456'; + const body = buildMultipartBody(boundary, [ + { name: 'userId', value: 'abc123' }, + { name: 'count', value: '5' }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartText`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.userId).toBe('abc123'); + expect(result.data.result.count).toBe('5'); + }); + + it('should parse file fields from multipart request', async () => { + Parse.Cloud.define('multipartFile', req => { + const file = req.params.avatar; + return { + filename: file.filename, + contentType: file.contentType, + size: file.data.length, + content: file.data.toString('utf8'), + }; + }); + + const boundary = '----TestBoundary789'; + const fileContent = Buffer.from('hello world'); + const body = buildMultipartBody(boundary, [ + { name: 'avatar', filename: 'photo.txt', contentType: 'text/plain', data: fileContent }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartFile`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.filename).toBe('photo.txt'); + expect(result.data.result.contentType).toBe('text/plain'); + expect(result.data.result.size).toBe(11); + expect(result.data.result.content).toBe('hello world'); + }); + + it('should parse mixed text and file fields from multipart request', async () => { + Parse.Cloud.define('multipartMixed', req => { + return { + userId: req.params.userId, + hasAvatar: !!req.params.avatar, + avatarFilename: req.params.avatar.filename, + }; + }); + + const boundary = '----TestBoundaryMixed'; + const body = buildMultipartBody(boundary, [ + { name: 'userId', value: 'user42' }, + { name: 'avatar', filename: 'img.jpg', contentType: 'image/jpeg', data: Buffer.from([0xff, 0xd8, 0xff]) }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartMixed`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.userId).toBe('user42'); + expect(result.data.result.hasAvatar).toBe(true); + expect(result.data.result.avatarFilename).toBe('img.jpg'); + }); + + it('should parse multiple file fields from multipart request', async () => { + Parse.Cloud.define('multipartMultiFile', req => { + return { + file1Name: req.params.doc1.filename, + file2Name: req.params.doc2.filename, + file1Size: req.params.doc1.data.length, + file2Size: req.params.doc2.data.length, + }; + }); + + const boundary = '----TestBoundaryMulti'; + const body = buildMultipartBody(boundary, [ + { name: 'doc1', filename: 'a.txt', contentType: 'text/plain', data: Buffer.from('aaa') }, + { name: 'doc2', filename: 'b.txt', contentType: 'text/plain', data: Buffer.from('bbbbb') }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartMultiFile`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.file1Name).toBe('a.txt'); + expect(result.data.result.file2Name).toBe('b.txt'); + expect(result.data.result.file1Size).toBe(3); + expect(result.data.result.file2Size).toBe(5); + }); + + it('should handle empty file field from multipart request', async () => { + Parse.Cloud.define('multipartEmptyFile', req => { + return { + filename: req.params.empty.filename, + size: req.params.empty.data.length, + }; + }); + + const boundary = '----TestBoundaryEmpty'; + const body = buildMultipartBody(boundary, [ + { name: 'empty', filename: 'empty.bin', contentType: 'application/octet-stream', data: Buffer.alloc(0) }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartEmptyFile`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.filename).toBe('empty.bin'); + expect(result.data.result.size).toBe(0); + }); + + it('should still handle JSON requests as before', async () => { + Parse.Cloud.define('jsonTest', req => { + return { name: req.params.name, count: req.params.count }; + }); + + const result = await Parse.Cloud.run('jsonTest', { name: 'hello', count: 42 }); + + expect(result.name).toBe('hello'); + expect(result.count).toBe(42); + }); + + it('should reject multipart request exceeding maxUploadSize', async () => { + await reconfigureServer({ maxUploadSize: '1kb' }); + + Parse.Cloud.define('multipartLarge', req => { + return { ok: true }; + }); + + const boundary = '----TestBoundaryLarge'; + const largeData = Buffer.alloc(2 * 1024, 'x'); + const body = buildMultipartBody(boundary, [ + { name: 'bigfile', filename: 'large.bin', contentType: 'application/octet-stream', data: largeData }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartLarge`, + { + '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 malformed multipart body', async () => { + Parse.Cloud.define('multipartMalformed', req => { + return { ok: true }; + }); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartMalformed`, + { + 'Content-Type': 'multipart/form-data; boundary=----TestBoundaryBad', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + Buffer.from('this is not valid multipart data') + ); + + expect(result.data.code).toBe(Parse.Error.INVALID_JSON); + }); +}); diff --git a/src/ParseServer.ts b/src/ParseServer.ts index 750c920556..65d537ae68 100644 --- a/src/ParseServer.ts +++ b/src/ParseServer.ts @@ -329,7 +329,7 @@ class ParseServer { new PagesRouter(pages).expressRouter() ); - api.use(express.json({ type: '*/*', limit: maxUploadSize })); + api.use(express.json({ type: req => !req.is('multipart/form-data'), limit: maxUploadSize })); api.use(middlewares.allowMethodOverride); api.use(middlewares.handleParseHeaders); api.use(middlewares.enforceRouteAllowList); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index ee1b7f9d5b..a6834037a7 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -9,6 +9,8 @@ import { jobStatusHandler } from '../StatusHandler'; import _ from 'lodash'; import { logger } from '../logger'; import { createSanitizedError } from '../Error'; +import Busboy from '@fastify/busboy'; +import Utils from '../Utils'; function parseObject(obj, config) { if (Array.isArray(obj)) { @@ -29,6 +31,8 @@ function parseObject(obj, config) { className: obj.className, objectId: obj.objectId, }); + } else if (Buffer.isBuffer(obj)) { + return obj; } else if (obj && typeof obj === 'object') { return parseParams(obj, config); } else { @@ -46,6 +50,7 @@ export class FunctionsRouter extends PromiseRouter { 'POST', '/functions/:functionName', promiseEnsureIdempotency, + FunctionsRouter.multipartMiddleware, FunctionsRouter.handleCloudFunction ); this.route( @@ -162,6 +167,81 @@ export class FunctionsRouter extends PromiseRouter { }; return responseObject; } + + static multipartMiddleware(req) { + if (!req.is || !req.is('multipart/form-data')) { + return Promise.resolve(); + } + const maxBytes = Utils.parseSizeToBytes(req.config.maxUploadSize); + return new Promise((resolve, reject) => { + const fields = {}; + let totalBytes = 0; + let settled = false; + let busboy; + try { + busboy = Busboy({ headers: req.headers }); + } catch (err) { + return reject( + new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`) + ); + } + const safeReject = (err) => { + if (settled) return; + settled = true; + busboy.destroy(); + reject(err); + }; + busboy.on('field', (name, value) => { + totalBytes += Buffer.byteLength(value); + if (totalBytes > maxBytes) { + return safeReject( + new Parse.Error( + Parse.Error.OBJECT_TOO_LARGE, + 'Multipart request exceeds maximum upload size.' + ) + ); + } + fields[name] = value; + }); + busboy.on('file', (name, stream, filename, transferEncoding, mimeType) => { + const chunks = []; + stream.on('data', chunk => { + totalBytes += chunk.length; + if (totalBytes > maxBytes) { + stream.destroy(); + return safeReject( + new Parse.Error( + Parse.Error.OBJECT_TOO_LARGE, + 'Multipart request exceeds maximum upload size.' + ) + ); + } + chunks.push(chunk); + }); + stream.on('end', () => { + if (settled) return; + fields[name] = { + filename, + contentType: mimeType || 'application/octet-stream', + data: Buffer.concat(chunks), + }; + }); + }); + busboy.on('finish', () => { + if (settled) return; + settled = true; + req.body = fields; + resolve(); + }); + busboy.on('error', err => { + safeReject( + new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`) + ); + }); + req.pipe(busboy); + }); + } + static handleCloudFunction(req) { const functionName = req.params.functionName; const applicationId = req.config.applicationId; From 2576341895ac19360fd103c6d02fcb9c8cd745ee Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:39:46 +0100 Subject: [PATCH 03/10] docs --- src/Routers/FunctionsRouter.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index a6834037a7..1d210f2ae9 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -168,6 +168,17 @@ export class FunctionsRouter extends PromiseRouter { return responseObject; } + /** + * Parses multipart/form-data requests for Cloud Function invocation. + * For non-multipart requests, this is a no-op. + * + * Text fields are set as strings in `req.body`. File fields are set as + * objects with the shape `{ filename: string, contentType: string, data: Buffer }`. + * All fields are merged flat into `req.body`; the caller is responsible for + * avoiding name collisions between text and file fields. + * + * The total request size is limited by the server's `maxUploadSize` option. + */ static multipartMiddleware(req) { if (!req.is || !req.is('multipart/form-data')) { return Promise.resolve(); From d7145d40f426ce995983679075edab0225039e64 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:42:17 +0100 Subject: [PATCH 04/10] docs --- src/cloud-code/Parse.Cloud.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 16f541cfeb..e829a492e0 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -821,7 +821,9 @@ module.exports = ParseCloud; * @property {Boolean} master If true, means the master key or the read-only master key was used. * @property {Boolean} isReadOnly If true, means the read-only master key was used. This is a subset of `master`, so `master` will also be true. Use `master && !isReadOnly` to check for full master key access. * @property {Parse.User} user If set, the user that made the request. - * @property {Object} params The params passed to the cloud function. + * @property {Object} params The params passed to the cloud function. For JSON requests, values + * retain their JSON types. For multipart/form-data requests, text fields are strings and file + * fields are objects with `{ filename: string, contentType: string, data: Buffer }`. * @property {String} ip The IP address of the client making the request. * @property {Object} headers The original HTTP headers for the request. * @property {Object} log The current logger inside Parse Server. From bf2104f4d20d713cd3a3e83ff22cb026e575532f Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:43:54 +0100 Subject: [PATCH 05/10] lint --- src/Routers/FunctionsRouter.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 1d210f2ae9..484b1af832 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -197,7 +197,9 @@ export class FunctionsRouter extends PromiseRouter { ); } const safeReject = (err) => { - if (settled) return; + if (settled) { + return; + } settled = true; busboy.destroy(); reject(err); @@ -230,7 +232,9 @@ export class FunctionsRouter extends PromiseRouter { chunks.push(chunk); }); stream.on('end', () => { - if (settled) return; + if (settled) { + return; + } fields[name] = { filename, contentType: mimeType || 'application/octet-stream', @@ -239,7 +243,9 @@ export class FunctionsRouter extends PromiseRouter { }); }); busboy.on('finish', () => { - if (settled) return; + if (settled) { + return; + } settled = true; req.body = fields; resolve(); From 4dc43492e200cf9d87b8c236012c799947659564 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:49:04 +0100 Subject: [PATCH 06/10] proto --- spec/CloudCodeMultipart.spec.js | 56 +++++++++++++++++++++++++++++++++ src/Routers/FunctionsRouter.js | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/spec/CloudCodeMultipart.spec.js b/spec/CloudCodeMultipart.spec.js index 16570ef6ee..d96cee1d96 100644 --- a/spec/CloudCodeMultipart.spec.js +++ b/spec/CloudCodeMultipart.spec.js @@ -281,4 +281,60 @@ describe('Cloud Code Multipart', () => { expect(result.data.code).toBe(Parse.Error.INVALID_JSON); }); + + it('should not allow prototype pollution via __proto__ field name', async () => { + Parse.Cloud.define('multipartProto', req => { + const obj = {}; + return { + polluted: obj.polluted !== undefined, + paramsClean: Object.getPrototypeOf(req.params) === Object.prototype, + }; + }); + + const boundary = '----TestBoundaryProto'; + const body = buildMultipartBody(boundary, [ + { name: '__proto__', value: '{"polluted":"yes"}' }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartProto`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.polluted).toBe(false); + expect(result.data.result.paramsClean).toBe(true); + }); + + it('should not grant master key access via multipart fields', async () => { + const obj = new Parse.Object('SecretClass'); + await obj.save(null, { useMasterKey: true }); + + Parse.Cloud.define('multipartAuthCheck', req => { + return { isMaster: req.master }; + }); + + const boundary = '----TestBoundaryAuth'; + const body = buildMultipartBody(boundary, [ + { name: '_MasterKey', value: 'test' }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartAuthCheck`, + { + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + body + ); + + expect(result.status).toBe(200); + expect(result.data.result.isMaster).toBe(false); + }); }); diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 484b1af832..d2acabf4ee 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -185,7 +185,7 @@ export class FunctionsRouter extends PromiseRouter { } const maxBytes = Utils.parseSizeToBytes(req.config.maxUploadSize); return new Promise((resolve, reject) => { - const fields = {}; + const fields = Object.create(null); let totalBytes = 0; let settled = false; let busboy; From 39f7d24f22f3ddc691aa9ea3f119646795c44596 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:58:47 +0100 Subject: [PATCH 07/10] test --- spec/CloudCodeMultipart.spec.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/CloudCodeMultipart.spec.js b/spec/CloudCodeMultipart.spec.js index d96cee1d96..b2f60c0761 100644 --- a/spec/CloudCodeMultipart.spec.js +++ b/spec/CloudCodeMultipart.spec.js @@ -264,6 +264,32 @@ describe('Cloud Code Multipart', () => { expect(result.data.code).toBe(Parse.Error.OBJECT_TOO_LARGE); }); + it('should reject multipart request exceeding maxUploadSize via file stream', async () => { + await reconfigureServer({ maxUploadSize: '1kb' }); + + Parse.Cloud.define('multipartLargeFile', req => { + return { ok: true }; + }); + + const boundary = '----TestBoundaryLargeFile'; + const body = buildMultipartBody(boundary, [ + { name: 'small', value: 'ok' }, + { name: 'bigfile', filename: 'large.bin', contentType: 'application/octet-stream', data: Buffer.alloc(2 * 1024, 'x') }, + ]); + + const result = await postMultipart( + `http://localhost:8378/1/functions/multipartLargeFile`, + { + '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 malformed multipart body', async () => { Parse.Cloud.define('multipartMalformed', req => { return { ok: true }; From e75a448fd55986be91e142e7d27a3b8ee5c5a63d Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:39:34 +0100 Subject: [PATCH 08/10] https://github.com/parse-community/parse-server/pull/10395#discussion_r3034210257 --- src/Routers/FunctionsRouter.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d2acabf4ee..0526b2cbb5 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -12,6 +12,23 @@ import { createSanitizedError } from '../Error'; import Busboy from '@fastify/busboy'; import Utils from '../Utils'; +function redactBuffers(obj) { + if (Buffer.isBuffer(obj)) { + return `[Buffer: ${obj.length} bytes]`; + } + if (Array.isArray(obj)) { + return obj.map(redactBuffers); + } + if (obj && typeof obj === 'object') { + const result = {}; + for (const key of Object.keys(obj)) { + result[key] = redactBuffers(obj[key]); + } + return result; + } + return obj; +} + function parseObject(obj, config) { if (Array.isArray(obj)) { return obj.map(item => { @@ -289,7 +306,7 @@ export class FunctionsRouter extends PromiseRouter { result => { try { if (req.config.logLevels.cloudFunctionSuccess !== 'silent') { - const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); + const cleanInput = logger.truncateLogMessage(JSON.stringify(redactBuffers(params))); const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result)); logger[req.config.logLevels.cloudFunctionSuccess]( `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, @@ -308,7 +325,7 @@ export class FunctionsRouter extends PromiseRouter { error => { try { if (req.config.logLevels.cloudFunctionError !== 'silent') { - const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); + const cleanInput = logger.truncateLogMessage(JSON.stringify(redactBuffers(params))); logger[req.config.logLevels.cloudFunctionError]( `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + JSON.stringify(error), From 3b956cba9e832ca1d90e2739fabf94f3af3a7182 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 4 Apr 2026 15:43:47 +0100 Subject: [PATCH 09/10] fix https://github.com/parse-community/parse-server/pull/10395#discussion_r3034210262 --- src/Routers/FunctionsRouter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 0526b2cbb5..428270f4c5 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -207,7 +207,7 @@ export class FunctionsRouter extends PromiseRouter { let settled = false; let busboy; try { - busboy = Busboy({ headers: req.headers }); + busboy = Busboy({ headers: req.headers, limits: { fieldSize: maxBytes } }); } catch (err) { return reject( new Parse.Error(Parse.Error.INVALID_JSON, `Invalid multipart request: ${err.message}`) From 7360becde758c173770235449b7bfa98ed1be269 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:16:01 +0100 Subject: [PATCH 10/10] fix https://github.com/parse-community/parse-server/pull/10395#discussion_r3035658907 --- src/Routers/FunctionsRouter.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index 428270f4c5..8bcf8a5858 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -221,7 +221,15 @@ export class FunctionsRouter extends PromiseRouter { busboy.destroy(); reject(err); }; - busboy.on('field', (name, value) => { + busboy.on('field', (name, value, fieldnameTruncated, valueTruncated) => { + if (valueTruncated) { + return safeReject( + new Parse.Error( + Parse.Error.OBJECT_TOO_LARGE, + 'Multipart request exceeds maximum upload size.' + ) + ); + } totalBytes += Buffer.byteLength(value); if (totalBytes > maxBytes) { return safeReject(