diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5aaa5ff9cc..a2be99ec13 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ on:
paths-ignore:
- '**/**.md'
env:
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
PARSE_SERVER_TEST_TIMEOUT: 20000
permissions:
actions: write
@@ -156,20 +156,20 @@ jobs:
- name: MongoDB 6, ReplicaSet
MONGODB_VERSION: 6.0.19
MONGODB_TOPOLOGY: replset
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: MongoDB 7, ReplicaSet
MONGODB_VERSION: 7.0.16
MONGODB_TOPOLOGY: replset
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: MongoDB 8, ReplicaSet
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: replset
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: Redis Cache
PARSE_SERVER_TEST_CACHE: redis
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: standalone
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: Node 20
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: standalone
@@ -178,6 +178,10 @@ jobs:
MONGODB_VERSION: 8.0.4
MONGODB_TOPOLOGY: standalone
NODE_VERSION: 18.20.4
+ - name: Node 22
+ MONGODB_VERSION: 8.0.4
+ MONGODB_TOPOLOGY: standalone
+ NODE_VERSION: 22.12.0
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 20
@@ -225,22 +229,22 @@ jobs:
include:
- name: PostgreSQL 15, PostGIS 3.3
POSTGRES_IMAGE: postgis/postgis:15-3.3
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: PostgreSQL 15, PostGIS 3.4
POSTGRES_IMAGE: postgis/postgis:15-3.4
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: PostgreSQL 15, PostGIS 3.5
POSTGRES_IMAGE: postgis/postgis:15-3.5
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: PostgreSQL 16, PostGIS 3.5
POSTGRES_IMAGE: postgis/postgis:16-3.5
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: PostgreSQL 17, PostGIS 3.5
POSTGRES_IMAGE: postgis/postgis:17-3.5
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
- name: PostgreSQL 18, PostGIS 3.6
POSTGRES_IMAGE: postgis/postgis:18-3.6
- NODE_VERSION: 22.12.0
+ NODE_VERSION: 24.11.0
fail-fast: false
name: ${{ matrix.name }}
timeout-minutes: 20
diff --git a/.github/workflows/release-manual-docs.yml b/.github/workflows/release-manual-docs.yml
new file mode 100644
index 0000000000..82c5b45e36
--- /dev/null
+++ b/.github/workflows/release-manual-docs.yml
@@ -0,0 +1,44 @@
+# Trigger this workflow only to manually create a docs release; this should only be used
+# in extraordinary circumstances, as docs releases are normally created automatically as
+# part of the automated release workflow.
+
+name: release-manual-docs
+on:
+ workflow_dispatch:
+ inputs:
+ ref:
+ default: ''
+ description: 'Reference (tag / SHA):'
+ required: true
+jobs:
+ docs:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.inputs.ref }}
+ - name: Use Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 18.20.4
+ - name: Cache Node.js modules
+ uses: actions/cache@v4
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-node-
+ - name: Generate Docs
+ run: |
+ echo $SOURCE_TAG
+ npm ci
+ ./release_docs.sh
+ env:
+ SOURCE_TAG: ${{ github.event.inputs.ref }}
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v3.7.3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./docs
diff --git a/README.md b/README.md
index 9103e0ecfb..ad51487019 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
[](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
[](https://github.com/parse-community/parse-dashboard/releases)
-[](https://nodejs.org)
+[](https://nodejs.org)
[](https://www.mongodb.com)
[](https://www.postgresql.org)
@@ -130,6 +130,7 @@ Parse Server is continuously tested with the most recent releases of Node.js to
| Node.js 18 | 18.20.4 | April 2025 | <= 8.x (2025) |
| Node.js 20 | 20.18.0 | April 2026 | <= 9.x (2026) |
| Node.js 22 | 22.12.0 | April 2027 | <= 10.x (2027) |
+| Node.js 24 | 24.11.0 | April 2028 | <= 11.x (2028) |
#### MongoDB
diff --git a/changelogs/CHANGELOG_alpha.md b/changelogs/CHANGELOG_alpha.md
index f1209f668b..bf414dc3f8 100644
--- a/changelogs/CHANGELOG_alpha.md
+++ b/changelogs/CHANGELOG_alpha.md
@@ -1,3 +1,24 @@
+# [8.4.0-alpha.2](https://github.com/parse-community/parse-server/compare/8.4.0-alpha.1...8.4.0-alpha.2) (2025-11-05)
+
+
+### Bug Fixes
+
+* Uploading a file by providing an origin URL allows for Server-Side Request Forgery (SSRF); fixes vulnerability [GHSA-x4qj-2f4q-r4rx](https://github.com/parse-community/parse-server/security/advisories/GHSA-x4qj-2f4q-r4rx) ([#9903](https://github.com/parse-community/parse-server/issues/9903)) ([9776386](https://github.com/parse-community/parse-server/commit/97763863b72689a29ad7a311dfb590c3e3c50585))
+
+# [8.4.0-alpha.1](https://github.com/parse-community/parse-server/compare/8.3.1-alpha.1...8.4.0-alpha.1) (2025-11-05)
+
+
+### Features
+
+* Add support for Node 24 ([#9901](https://github.com/parse-community/parse-server/issues/9901)) ([25dfe19](https://github.com/parse-community/parse-server/commit/25dfe19fef02fd44224e4a6d198584a694a1aa52))
+
+## [8.3.1-alpha.1](https://github.com/parse-community/parse-server/compare/8.3.0...8.3.1-alpha.1) (2025-11-05)
+
+
+### Bug Fixes
+
+* Add problematic MIME types to default value of Parse Server option `fileUpload.fileExtensions` ([#9902](https://github.com/parse-community/parse-server/issues/9902)) ([fa245cb](https://github.com/parse-community/parse-server/commit/fa245cbb5f5b7a0dad962b2ce0524fa4dafcb5f7))
+
# [8.3.0-alpha.14](https://github.com/parse-community/parse-server/compare/8.3.0-alpha.13...8.3.0-alpha.14) (2025-11-01)
diff --git a/jsdoc-conf.json b/jsdoc-conf.json
index 52bb51f4dc..b410d239b0 100644
--- a/jsdoc-conf.json
+++ b/jsdoc-conf.json
@@ -6,10 +6,10 @@
"source": {
"include": [
"README.md",
- "./src/cloud-code",
- "./src/Options/docs.js",
- "./src/ParseServer.js",
- "./src/Adapters"
+ "./lib/cloud-code",
+ "./lib/Options/docs.js",
+ "./lib/ParseServer.js",
+ "./lib/Adapters"
],
"excludePattern": "(^|\\/|\\\\)_"
},
diff --git a/package-lock.json b/package-lock.json
index 37f3585512..a8fb041497 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "parse-server",
- "version": "8.3.0",
+ "version": "8.4.0-alpha.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
- "version": "8.3.0",
+ "version": "8.4.0-alpha.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -107,7 +107,7 @@
"yaml": "2.8.0"
},
"engines": {
- "node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0"
+ "node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0"
},
"funding": {
"type": "opencollective",
diff --git a/package.json b/package.json
index 559196a02c..35a3f461d0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "parse-server",
- "version": "8.3.0",
+ "version": "8.4.0-alpha.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
@@ -142,7 +142,7 @@
},
"types": "types/index.d.ts",
"engines": {
- "node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0"
+ "node": ">=18.20.4 <19.0.0 || >=20.18.0 <21.0.0 || >=22.12.0 <23.0.0 || >=24.11.0 <25.0.0"
},
"bin": {
"parse-server": "bin/parse-server"
diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js
index 6be506be8d..d6539b7336 100644
--- a/spec/ParseFile.spec.js
+++ b/spec/ParseFile.spec.js
@@ -653,6 +653,80 @@ describe('Parse.File testing', () => {
done();
});
});
+
+ describe('URI-backed file upload is disabled to prevent SSRF attack', () => {
+ const express = require('express');
+ let testServer;
+ let testServerPort;
+ let requestsMade;
+
+ beforeEach(async () => {
+ requestsMade = [];
+ const app = express();
+ app.use((req, res) => {
+ requestsMade.push({ url: req.url, method: req.method });
+ res.status(200).send('test file content');
+ });
+ testServer = app.listen(0);
+ testServerPort = testServer.address().port;
+ });
+
+ afterEach(async () => {
+ if (testServer) {
+ await new Promise(resolve => testServer.close(resolve));
+ }
+ Parse.Cloud._removeAllHooks();
+ });
+
+ it('does not access URI when file upload attempted over REST', async () => {
+ const response = await request({
+ method: 'POST',
+ url: 'http://localhost:8378/1/classes/TestClass',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ },
+ body: {
+ file: {
+ __type: 'File',
+ name: 'test.txt',
+ _source: {
+ format: 'uri',
+ uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
+ },
+ },
+ },
+ });
+ expect(response.status).toBe(201);
+ // Verify no HTTP request was made to the URI
+ expect(requestsMade.length).toBe(0);
+ });
+
+ it('does not access URI when file created in beforeSave trigger', async () => {
+ Parse.Cloud.beforeSave(Parse.File, () => {
+ return new Parse.File('trigger-file.txt', {
+ uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
+ });
+ });
+ await expectAsync(
+ request({
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ 'X-Parse-Application-Id': 'test',
+ 'X-Parse-REST-API-Key': 'rest',
+ },
+ url: 'http://localhost:8378/1/files/test.txt',
+ body: 'test content',
+ })
+ ).toBeRejectedWith(jasmine.objectContaining({
+ status: 400
+ }));
+ // Verify no HTTP request was made to the URI
+ expect(requestsMade.length).toBe(0);
+ });
+ });
});
describe('deleting files', () => {
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index d2f1e6eef1..d5674eaf29 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -1077,9 +1077,9 @@ module.exports.FileUploadOptions = {
fileExtensions: {
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS',
help:
- "Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?!(h|H)(t|T)(m|M)(l|L)?$)` which allows any file extension except HTML files.",
+ "Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser.",
action: parsers.arrayParser,
- default: ['^(?!(h|H)(t|T)(m|M)(l|L)?$)'],
+ default: ['^(?![xXsS]?[hH][tT][mM][lL]?$)'],
},
};
module.exports.DatabaseOptions = {
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 9e650b1038..4d268847b1 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -235,7 +235,7 @@
* @property {Boolean} enableForAnonymousUser Is true if file upload should be allowed for anonymous users.
* @property {Boolean} enableForAuthenticatedUser Is true if file upload should be allowed for authenticated users.
* @property {Boolean} enableForPublic Is true if file upload should be allowed for anyone, regardless of user authentication.
- * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?!(h|H)(t|T)(m|M)(l|L)?$)` which allows any file extension except HTML files.
+ * @property {String[]} fileExtensions Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser.
*/
/**
diff --git a/src/Options/index.js b/src/Options/index.js
index 42da7b2237..d5317646ba 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -594,8 +594,8 @@ export interface PasswordPolicyOptions {
}
export interface FileUploadOptions {
- /* Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?!(h|H)(t|T)(m|M)(l|L)?$)` which allows any file extension except HTML files.
- :DEFAULT: ["^(?!(h|H)(t|T)(m|M)(l|L)?$)"] */
+ /* Sets the allowed file extensions for uploading files. The extension is defined as an array of file extensions, or a regex pattern.
It is recommended to restrict the file upload extensions as much as possible. HTML files are especially problematic as they may be used by an attacker who uploads a HTML form to look legitimate under your app's domain name, or to compromise the session token of another user via accessing the browser's local storage.
Defaults to `^(?![xXsS]?[hH][tT][mM][lL]?$)` which allows any file extension except those MIME types that are mapped to `text/html` and are rendered as website by a web browser.
+ :DEFAULT: ["^(?![xXsS]?[hH][tT][mM][lL]?$)"] */
fileExtensions: ?(string[]);
/* Is true if file upload should be allowed for anonymous users.
:DEFAULT: false */
diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js
index 0bec64c9aa..5cb39abf47 100644
--- a/src/Routers/FilesRouter.js
+++ b/src/Routers/FilesRouter.js
@@ -4,34 +4,8 @@ import Parse from 'parse/node';
import Config from '../Config';
import logger from '../logger';
const triggers = require('../triggers');
-const http = require('http');
const Utils = require('../Utils');
-const downloadFileFromURI = uri => {
- return new Promise((res, rej) => {
- http
- .get(uri, response => {
- response.setDefaultEncoding('base64');
- let body = `data:${response.headers['content-type']};base64,`;
- response.on('data', data => (body += data));
- response.on('end', () => res(body));
- })
- .on('error', e => {
- rej(`Error downloading file from ${uri}: ${e.message}`);
- });
- });
-};
-
-const addFileDataIfNeeded = async file => {
- if (file._source.format === 'uri') {
- const base64 = await downloadFileFromURI(file._source.uri);
- file._previousSave = file;
- file._data = base64;
- file._requestTask = null;
- }
- return file;
-};
-
export class FilesRouter {
expressRouter({ maxUploadSize = '20Mb' } = {}) {
var router = express.Router();
@@ -247,8 +221,6 @@ export class FilesRouter {
}
// if the file returned by the trigger has already been saved skip saving anything
if (!saveResult) {
- // if the ParseFile returned is type uri, download the file before saving it
- await addFileDataIfNeeded(fileObject.file);
// update fileSize
const bufferData = Buffer.from(fileObject.file._data, 'base64');
fileObject.fileSize = Buffer.byteLength(bufferData);