feat: Upgrade to express 5.0.1 (#9530)

BREAKING CHANGE: This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info.
This commit is contained in:
Colin Ulin
2025-03-03 16:11:42 -05:00
committed by GitHub
parent cc8dad8fa1
commit e0480dfa8d
26 changed files with 995 additions and 401 deletions

1261
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,10 @@
"@parse/fs-files-adapter": "3.0.0",
"@parse/push-adapter": "6.10.0",
"bcryptjs": "2.4.3",
"body-parser": "1.20.3",
"commander": "13.0.0",
"cors": "2.8.5",
"deepcopy": "2.1.0",
"express": "4.21.2",
"express": "5.0.1",
"express-rate-limit": "7.5.0",
"follow-redirects": "1.15.9",
"graphql": "16.9.0",
@@ -58,6 +57,7 @@
"punycode": "2.3.1",
"rate-limit-redis": "4.2.0",
"redis": "4.7.0",
"router": "2.0.0",
"semver": "7.7.1",
"subscriptions-transport-ws": "0.11.0",
"tv4": "1.3.0",

View File

@@ -2,7 +2,6 @@
const httpRequest = require('../lib/request'),
HTTPResponse = require('../lib/request').HTTPResponse,
bodyParser = require('body-parser'),
express = require('express');
const port = 13371;
@@ -10,7 +9,7 @@ const httpRequestServer = `http://localhost:${port}`;
function startServer(done) {
const app = express();
app.use(bodyParser.json({ type: '*/*' }));
app.use(express.json({ type: '*/*' }));
app.get('/hello', function (req, res) {
res.json({ response: 'OK' });
});

View File

@@ -4,7 +4,6 @@ const request = require('../lib/request');
const triggers = require('../lib/triggers');
const HooksController = require('../lib/Controllers/HooksController').default;
const express = require('express');
const bodyParser = require('body-parser');
const auth = require('../lib/Auth');
const Config = require('../lib/Config');
@@ -17,7 +16,7 @@ describe('Hooks', () => {
beforeEach(done => {
if (!app) {
app = express();
app.use(bodyParser.json({ type: '*/*' }));
app.use(express.json({ type: '*/*' }));
server = app.listen(port, undefined, done);
} else {
done();

View File

@@ -250,11 +250,10 @@ describe('Vulnerabilities', () => {
it_id('e8b5f1e1-8326-4c70-b5f4-1e8678dfff8d')(it)('denies creating a hook with polluted data', async () => {
const express = require('express');
const bodyParser = require('body-parser');
const port = 34567;
const hookServerURL = 'http://localhost:' + port;
const app = express();
app.use(bodyParser.json({ type: '*/*' }));
app.use(express.json({ type: '*/*' }));
const server = await new Promise(resolve => {
const res = app.listen(port, undefined, () => resolve(res));
});

View File

@@ -5,7 +5,7 @@ export class AnalyticsController extends AdaptableController {
appOpened(req) {
return Promise.resolve()
.then(() => {
return this.adapter.appOpened(req.body, req);
return this.adapter.appOpened(req.body || {}, req);
})
.then(response => {
return { response: response || {} };
@@ -18,7 +18,7 @@ export class AnalyticsController extends AdaptableController {
trackEvent(req) {
return Promise.resolve()
.then(() => {
return this.adapter.trackEvent(req.params.eventName, req.body, req);
return this.adapter.trackEvent(req.params.eventName, req.body || {}, req);
})
.then(response => {
return { response: response || {} };

View File

@@ -1,7 +1,6 @@
// ParseServer - open-source compatible API Server for Parse apps
var batch = require('./batch'),
bodyParser = require('body-parser'),
express = require('express'),
middlewares = require('./middlewares'),
Parse = require('parse/node').Parse,
@@ -253,6 +252,7 @@ class ParseServer {
var api = express();
//api.use("/apps", express.static(__dirname + "/public"));
api.use(middlewares.allowCrossDomain(appId));
api.use(middlewares.allowDoubleForwardSlash);
// File handling needs to be before default middlewares are applied
api.use(
'/',
@@ -273,15 +273,16 @@ class ParseServer {
api.use(
'/',
bodyParser.urlencoded({ extended: false }),
express.urlencoded({ extended: false }),
pages.enableRouter
? new PagesRouter(pages).expressRouter()
: new PublicAPIRouter().expressRouter()
);
api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize }));
api.use(express.json({ type: '*/*', limit: maxUploadSize }));
api.use(middlewares.allowMethodOverride);
api.use(middlewares.handleParseHeaders);
api.set('query parser', 'extended');
const routes = Array.isArray(rateLimit) ? rateLimit : [rateLimit];
for (const route of routes) {
middlewares.addRateLimit(route, options);

View File

@@ -9,7 +9,7 @@ import Parse from 'parse/node';
import express from 'express';
import log from './logger';
import { inspect } from 'util';
const Layer = require('express/lib/router/layer');
const Layer = require('router/lib/layer');
function validateParameter(key, value) {
if (key == 'className') {

View File

@@ -6,7 +6,7 @@ import UsersRouter from './UsersRouter';
export class AggregateRouter extends ClassesRouter {
handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = {};
if (body.distinct) {
options.distinct = String(body.distinct);

View File

@@ -8,7 +8,7 @@ export class AudiencesRouter extends ClassesRouter {
}
handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit);
return rest

View File

@@ -19,7 +19,7 @@ export class ClassesRouter extends PromiseRouter {
}
handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit);
if (req.config.maxLimit && body.limit > req.config.maxLimit) {
// Silently replace the limit on the query with the max configured
@@ -48,7 +48,7 @@ export class ClassesRouter extends PromiseRouter {
// Returns a promise for a {response} object.
handleGet(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = {};
for (const key of Object.keys(body)) {
@@ -117,7 +117,7 @@ export class ClassesRouter extends PromiseRouter {
req.config,
req.auth,
this.className(req),
req.body,
req.body || {},
req.info.clientSDK,
req.info.context
);
@@ -130,7 +130,7 @@ export class ClassesRouter extends PromiseRouter {
req.auth,
this.className(req),
where,
req.body,
req.body || {},
req.info.clientSDK,
req.info.context
);

View File

@@ -77,7 +77,7 @@ export class CloudCodeRouter extends PromiseRouter {
}
static createJob(req) {
const { job_schedule } = req.body;
const { job_schedule } = req.body || {};
validateJobSchedule(req.config, job_schedule);
return rest.create(
req.config,
@@ -91,7 +91,7 @@ export class CloudCodeRouter extends PromiseRouter {
static editJob(req) {
const { objectId } = req.params;
const { job_schedule } = req.body;
const { job_schedule } = req.body || {};
validateJobSchedule(req.config, job_schedule);
return rest
.update(

View File

@@ -1,5 +1,4 @@
import express from 'express';
import BodyParser from 'body-parser';
import * as Middlewares from '../middlewares';
import Parse from 'parse/node';
import Config from '../Config';
@@ -45,7 +44,7 @@ export class FilesRouter {
router.post(
'/files/:filename',
BodyParser.raw({
express.raw({
type: () => {
return true;
},

View File

@@ -58,7 +58,7 @@ export class FunctionsRouter extends PromiseRouter {
}
static handleCloudJob(req) {
const jobName = req.params.jobName || req.body.jobName;
const jobName = req.params.jobName || req.body?.jobName;
const applicationId = req.config.applicationId;
const jobHandler = jobStatusHandler(req.config);
const jobFunction = triggers.getJob(jobName, applicationId);

View File

@@ -46,8 +46,8 @@ export class GlobalConfigRouter extends PromiseRouter {
"read-only masterKey isn't allowed to update the config."
);
}
const params = req.body.params;
const masterKeyOnly = req.body.masterKeyOnly || {};
const params = req.body.params || {};
const masterKeyOnly = req.body?.masterKeyOnly || {};
// Transform in dot notation to make sure it works
const update = Object.keys(params).reduce((acc, key) => {
acc[`params.${key}`] = params[key];

View File

@@ -19,7 +19,7 @@ export class GraphQLRouter extends PromiseRouter {
"read-only masterKey isn't allowed to update the GraphQL config."
);
}
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body.params);
const data = await req.config.parseGraphQLController.updateGraphQLConfig(req.body?.params || {});
return {
response: data,
};

View File

@@ -12,7 +12,7 @@ export class HooksRouter extends PromiseRouter {
}
handlePost(req) {
return this.createHook(req.body, req.config);
return this.createHook(req.body || {}, req.config);
}
handleGetFunctions(req) {
@@ -66,11 +66,11 @@ export class HooksRouter extends PromiseRouter {
handleUpdate(req) {
var hook;
if (req.params.functionName && req.body.url) {
if (req.params.functionName && req.body?.url) {
hook = {};
hook.functionName = req.params.functionName;
hook.url = req.body.url;
} else if (req.params.className && req.params.triggerName && req.body.url) {
} else if (req.params.className && req.params.triggerName && req.body?.url) {
hook = {};
hook.className = req.params.className;
hook.triggerName = req.params.triggerName;
@@ -82,7 +82,7 @@ export class HooksRouter extends PromiseRouter {
}
handlePut(req) {
var body = req.body;
var body = req.body || {};
if (body.__op == 'Delete') {
return this.handleDelete(req);
} else {

View File

@@ -68,8 +68,8 @@ function getFileForProductIdentifier(productIdentifier, req) {
export class IAPValidationRouter extends PromiseRouter {
handleRequest(req) {
let receipt = req.body.receipt;
const productIdentifier = req.body.productIdentifier;
let receipt = req.body?.receipt;
const productIdentifier = req.body?.productIdentifier;
if (!receipt || !productIdentifier) {
// TODO: Error, malformed request
@@ -84,7 +84,7 @@ export class IAPValidationRouter extends PromiseRouter {
}
}
if (process.env.TESTING == '1' && req.body.bypassAppStoreValidation) {
if (process.env.TESTING == '1' && req.body?.bypassAppStoreValidation) {
return getFileForProductIdentifier(productIdentifier, req);
}

View File

@@ -10,7 +10,7 @@ export class InstallationsRouter extends ClassesRouter {
}
handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
const body = Object.assign(req.body || {}, ClassesRouter.JSONFromQuery(req.query));
const options = ClassesRouter.optionsFromBody(body, req.config.defaultLimit);
return rest
.find(

View File

@@ -107,8 +107,8 @@ export class PagesRouter extends PromiseRouter {
resendVerificationEmail(req) {
const config = req.config;
const username = req.body.username;
const token = req.body.token;
const username = req.body?.username;
const token = req.body?.token;
if (!config) {
this.invalidRequest();
@@ -178,7 +178,7 @@ export class PagesRouter extends PromiseRouter {
this.invalidRequest();
}
const { new_password, token: rawToken } = req.body;
const { new_password, token: rawToken } = req.body || {};
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
if ((!token || !new_password) && req.xhr === false) {
@@ -320,7 +320,7 @@ export class PagesRouter extends PromiseRouter {
*/
staticRoute(req) {
// Get requested path
const relativePath = req.params[0];
const relativePath = req.params['resource'][0];
// Resolve requested path to absolute path
const absolutePath = path.resolve(this.pagesPath, relativePath);
@@ -716,7 +716,7 @@ export class PagesRouter extends PromiseRouter {
mountStaticRoute() {
this.route(
'GET',
`/${this.pagesEndpoint}/(*)?`,
`/${this.pagesEndpoint}/*resource`,
req => {
this.setConfig(req, true);
},

View File

@@ -52,7 +52,7 @@ export class PublicAPIRouter extends PromiseRouter {
}
resendVerificationEmail(req) {
const username = req.body.username;
const username = req.body?.username;
const appId = req.params.appId;
const config = Config.get(appId);
@@ -162,7 +162,7 @@ export class PublicAPIRouter extends PromiseRouter {
return this.missingPublicServerURL();
}
const { new_password, token: rawToken } = req.body;
const { new_password, token: rawToken } = req.body || {};
const token = rawToken && typeof rawToken !== 'string' ? rawToken.toString() : rawToken;
if ((!token || !new_password) && req.xhr === false) {

View File

@@ -26,7 +26,7 @@ export class PushRouter extends PromiseRouter {
});
let pushStatusId;
pushController
.sendPush(req.body, where, req.config, req.auth, objectId => {
.sendPush(req.body || {}, where, req.config, req.auth, objectId => {
pushStatusId = objectId;
resolve({
headers: {

View File

@@ -77,18 +77,18 @@ async function createSchema(req) {
"read-only masterKey isn't allowed to create a schema."
);
}
if (req.params.className && req.body.className) {
if (req.params.className && req.body?.className) {
if (req.params.className != req.body.className) {
return classNameMismatchResponse(req.body.className, req.params.className);
}
}
const className = req.params.className || req.body.className;
const className = req.params.className || req.body?.className;
if (!className) {
throw new Parse.Error(135, `POST ${req.path} needs a class name.`);
}
return await internalCreateSchema(className, req.body, req.config);
return await internalCreateSchema(className, req.body || {}, req.config);
}
function modifySchema(req) {
@@ -99,12 +99,12 @@ function modifySchema(req) {
"read-only masterKey isn't allowed to update a schema."
);
}
if (req.body.className && req.body.className != req.params.className) {
if (req.body?.className && req.body.className != req.params.className) {
return classNameMismatchResponse(req.body.className, req.params.className);
}
const className = req.params.className;
return internalUpdateSchema(className, req.body, req.config);
return internalUpdateSchema(className, req.body || {}, req.config);
}
const deleteSchema = req => {

View File

@@ -68,7 +68,7 @@ export class UsersRouter extends ClassesRouter {
_authenticateUserFromRequest(req) {
return new Promise((resolve, reject) => {
// Use query parameters instead if provided in url
let payload = req.body;
let payload = req.body || {};
if (
(!payload.username && req.query && req.query.username) ||
(!payload.email && req.query && req.query.email)
@@ -219,7 +219,7 @@ export class UsersRouter extends ClassesRouter {
req.auth,
'_User',
{ objectId: user.objectId },
req.body,
req.body || {},
user,
req.info.clientSDK,
req.info.context
@@ -336,7 +336,7 @@ export class UsersRouter extends ClassesRouter {
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'master key is required');
}
const userId = req.body.userId || req.query.userId;
const userId = req.body?.userId || req.query.userId;
if (!userId) {
throw new Parse.Error(
Parse.Error.INVALID_VALUE,
@@ -438,8 +438,9 @@ export class UsersRouter extends ClassesRouter {
async handleResetRequest(req) {
this._throwOnBadEmailConfig(req);
let email = req.body.email;
const token = req.body.token;
let email = req.body?.email;
const token = req.body?.token;
if (!email && !token) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
}
@@ -480,7 +481,7 @@ export class UsersRouter extends ClassesRouter {
async handleVerificationEmailRequest(req) {
this._throwOnBadEmailConfig(req);
const { email } = req.body;
const { email } = req.body || {};
if (!email) {
throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email');
}
@@ -513,7 +514,7 @@ export class UsersRouter extends ClassesRouter {
}
async handleChallenge(req) {
const { username, email, password, authData, challengeData } = req.body;
const { username, email, password, authData, challengeData } = req.body || {};
// if username or email provided with password try to authenticate the user by username
let user;

View File

@@ -64,7 +64,7 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) {
// Returns a promise for a {response} object.
// TODO: pass along auth correctly
function handleBatch(router, req) {
if (!Array.isArray(req.body.requests)) {
if (!Array.isArray(req.body?.requests)) {
throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array');
}
@@ -85,12 +85,12 @@ function handleBatch(router, req) {
const batch = transactionRetries => {
let initialPromise = Promise.resolve();
if (req.body.transaction === true) {
if (req.body?.transaction === true) {
initialPromise = req.config.database.createTransactionalSession();
}
return initialPromise.then(() => {
const promises = req.body.requests.map(restRequest => {
const promises = req.body?.requests.map(restRequest => {
const routablePath = makeRoutablePath(restRequest.path);
// Construct a request that we can send to a handler
@@ -113,7 +113,7 @@ function handleBatch(router, req) {
return Promise.all(promises)
.then(results => {
if (req.body.transaction === true) {
if (req.body?.transaction === true) {
if (results.find(result => typeof result.error === 'object')) {
return req.config.database.abortTransactionalSession().then(() => {
return Promise.reject({ response: results });

View File

@@ -196,7 +196,7 @@ export async function handleParseHeaders(req, res, next) {
info.clientSDK = ClientSDK.fromString(info.clientVersion);
}
if (fileViaJSON) {
if (fileViaJSON && req.body) {
req.fileData = req.body.fileData;
// We need to repopulate req.body with a buffer
var base64 = req.body.base64;
@@ -450,7 +450,7 @@ export function allowCrossDomain(appId) {
}
export function allowMethodOverride(req, res, next) {
if (req.method === 'POST' && req.body._method) {
if (req.method === 'POST' && req.body?._method) {
req.originalMethod = req.method;
req.method = req.body._method;
delete req.body._method;
@@ -685,3 +685,16 @@ function malformedContext(req, res) {
res.status(400);
res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' });
}
/**
* Express 4 allowed a double forward slash between a route and router. Although
* this should be considered an anti-pattern, we need to support it for backwards
* compatibility.
*
* Technically valid URL with double foroward slash:
* http://localhost:1337/parse//functions/testFunction
*/
export function allowDoubleForwardSlash(req, res, next) {
req.url = req.url.startsWith('//') ? req.url.substring(1) : req.url;
next();
}