Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/release-manual-docs.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Coverage](https://codecov.io/github/parse-community/parse-server/branch/alpha/graph/badge.svg)](https://app.codecov.io/github/parse-community/parse-server/tree/alpha)
[![auto-release](https://img.shields.io/badge/%F0%9F%9A%80-auto--release-9e34eb.svg)](https://github.com/parse-community/parse-dashboard/releases)

[![Node Version](https://img.shields.io/badge/nodejs-18,_20,_22-green.svg?logo=node.js&style=flat)](https://nodejs.org)
[![Node Version](https://img.shields.io/badge/nodejs-18,_20,_22,_24-green.svg?logo=node.js&style=flat)](https://nodejs.org)
[![MongoDB Version](https://img.shields.io/badge/mongodb-6,_7,_8-green.svg?logo=mongodb&style=flat)](https://www.mongodb.com)
[![Postgres Version](https://img.shields.io/badge/postgresql-13,_14,_15,_16,_17,_18-green.svg?logo=postgresql&style=flat)](https://www.postgresql.org)

Expand Down Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions changelogs/CHANGELOG_alpha.md
Original file line number Diff line number Diff line change
@@ -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)


Expand Down
8 changes: 4 additions & 4 deletions jsdoc-conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "(^|\\/|\\\\)_"
},
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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"
Expand Down
74 changes: 74 additions & 0 deletions spec/ParseFile.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/Options/Definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br><br>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.<br><br>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.<br><br>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.<br><br>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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/Options/docs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br><br>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.<br><br>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.<br><br>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.<br><br>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 */
Expand Down
28 changes: 0 additions & 28 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Loading