From 832702dffd265fb3958599d53528817bf9c3d887 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 1 May 2023 21:50:23 +0000 Subject: [PATCH 1/3] chore(release): 6.1.0 [skip ci] # [6.1.0](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0) (2023-05-01) ### Bug Fixes * LiveQuery can return incorrectly formatted date ([#8456](https://github.com/parse-community/parse-server/issues/8456)) ([4ce135a](https://github.com/parse-community/parse-server/commit/4ce135a4fe930776044bc8fd786a4e17a0144e03)) * Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf)) * Parameters missing in `afterFind` trigger of authentication adapters ([#8458](https://github.com/parse-community/parse-server/issues/8458)) ([ce34747](https://github.com/parse-community/parse-server/commit/ce34747e8af54cb0b6b975da38f779a5955d2d59)) * Rate limiting across multiple servers via Redis not working ([#8469](https://github.com/parse-community/parse-server/issues/8469)) ([d9e347d](https://github.com/parse-community/parse-server/commit/d9e347d7413f30f58ffbb8397fc8b5ae23be6ff0)) * Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164)) ### Features * Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33)) * Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323)) * Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b)) * Add rate limiting across multiple servers via Redis ([#8394](https://github.com/parse-community/parse-server/issues/8394)) ([34833e4](https://github.com/parse-community/parse-server/commit/34833e42eec08b812b733be78df0535ab0e096b6)) * Allow multiple origins for header `Access-Control-Allow-Origin` ([#8517](https://github.com/parse-community/parse-server/issues/8517)) ([4f15539](https://github.com/parse-community/parse-server/commit/4f15539ac244aa2d393ac5177f7604b43f69e271)) * Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1)) * Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6)) --- changelogs/CHANGELOG_release.md | 21 +++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index c7c35bec..4196602b 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,24 @@ +# [6.1.0](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0) (2023-05-01) + + +### Bug Fixes + +* LiveQuery can return incorrectly formatted date ([#8456](https://github.com/parse-community/parse-server/issues/8456)) ([4ce135a](https://github.com/parse-community/parse-server/commit/4ce135a4fe930776044bc8fd786a4e17a0144e03)) +* Nested date is incorrectly decoded as empty object `{}` when fetching a Parse Object ([#8446](https://github.com/parse-community/parse-server/issues/8446)) ([22d2446](https://github.com/parse-community/parse-server/commit/22d2446dfea2bc339affc20535d181097e152acf)) +* Parameters missing in `afterFind` trigger of authentication adapters ([#8458](https://github.com/parse-community/parse-server/issues/8458)) ([ce34747](https://github.com/parse-community/parse-server/commit/ce34747e8af54cb0b6b975da38f779a5955d2d59)) +* Rate limiting across multiple servers via Redis not working ([#8469](https://github.com/parse-community/parse-server/issues/8469)) ([d9e347d](https://github.com/parse-community/parse-server/commit/d9e347d7413f30f58ffbb8397fc8b5ae23be6ff0)) +* Security upgrade jsonwebtoken to 9.0.0 ([#8420](https://github.com/parse-community/parse-server/issues/8420)) ([f5bfe45](https://github.com/parse-community/parse-server/commit/f5bfe4571e82b2b7440d41f3cff0d49937398164)) + +### Features + +* Add `afterFind` trigger to authentication adapters ([#8444](https://github.com/parse-community/parse-server/issues/8444)) ([c793bb8](https://github.com/parse-community/parse-server/commit/c793bb88e7485743c7ceb65fe419cde75833ff33)) +* Add option `schemaCacheTtl` for schema cache pulling as alternative to `enableSchemaHooks` ([#8436](https://github.com/parse-community/parse-server/issues/8436)) ([b3b76de](https://github.com/parse-community/parse-server/commit/b3b76de71b1d4265689d052e7837c38ec1fa4323)) +* Add Parse Server option `resetPasswordSuccessOnInvalidEmail` to choose success or error response on password reset with invalid email ([#7551](https://github.com/parse-community/parse-server/issues/7551)) ([e5d610e](https://github.com/parse-community/parse-server/commit/e5d610e5e487ddab86409409ac3d7362aba8f59b)) +* Add rate limiting across multiple servers via Redis ([#8394](https://github.com/parse-community/parse-server/issues/8394)) ([34833e4](https://github.com/parse-community/parse-server/commit/34833e42eec08b812b733be78df0535ab0e096b6)) +* Allow multiple origins for header `Access-Control-Allow-Origin` ([#8517](https://github.com/parse-community/parse-server/issues/8517)) ([4f15539](https://github.com/parse-community/parse-server/commit/4f15539ac244aa2d393ac5177f7604b43f69e271)) +* Deprecate LiveQuery `fields` option in favor of `keys` for semantic consistency ([#8388](https://github.com/parse-community/parse-server/issues/8388)) ([a49e323](https://github.com/parse-community/parse-server/commit/a49e323d5ae640bff1c6603ec37fdaddb9328dd1)) +* Export `AuthAdapter` to make it available for extension with custom authentication adapters ([#8443](https://github.com/parse-community/parse-server/issues/8443)) ([40c1961](https://github.com/parse-community/parse-server/commit/40c196153b8efa12ae384c1c0092b2ed60a260d6)) + # [6.0.0](https://github.com/parse-community/parse-server/compare/5.4.0...6.0.0) (2023-01-31) diff --git a/package-lock.json b/package-lock.json index a3c6b0f3..9445d96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.1.0-beta.2", + "version": "6.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.1.0-beta.2", + "version": "6.1.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 8f7bbb5c..6981537b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.1.0-beta.2", + "version": "6.1.0", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From a318e7bbafcf7a3425b0a1b3c2dd30f526b4b6f9 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 21 May 2023 01:23:00 +0200 Subject: [PATCH 2/3] feat: Add new Parse Server option `fileUpload.fileExtensions` to restrict file upload by file extension; this fixes a security vulnerability in which a phishing attack could be performed using an uploaded HTML file; by default the new option only allows file extensions matching the regex pattern `^[^hH][^tT][^mM][^lL]?$`, which excludes HTML files; if your app currently depends on uploading files with HTML file extensions then this may be a breaking change and you could allow HTML file upload by setting the option to `['.*']` (#8538) --- spec/ParseFile.spec.js | 186 ++++++++++++++++++++++++++++++++----- src/Config.js | 5 + src/Options/Definitions.js | 7 ++ src/Options/docs.js | 1 + src/Options/index.js | 3 + src/Routers/FilesRouter.js | 32 +++++++ 6 files changed, 209 insertions(+), 25 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index ed21304d..eeab5370 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -37,8 +37,14 @@ describe('Parse.File testing', () => { }); }); - it('works with _ContentType', done => { - request({ + it('works with _ContentType', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['*'], + }, + }); + let response = await request({ method: 'POST', url: 'http://localhost:8378/1/files/file', body: JSON.stringify({ @@ -47,21 +53,18 @@ describe('Parse.File testing', () => { _ContentType: 'text/html', base64: 'PGh0bWw+PC9odG1sPgo=', }), - }).then(response => { - const b = response.data; - expect(b.name).toMatch(/_file.html/); - expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/); - request({ url: b.url }).then(response => { - const body = response.text; - try { - expect(response.headers['content-type']).toMatch('^text/html'); - expect(body).toEqual('\n'); - } catch (e) { - jfail(e); - } - done(); - }); }); + const b = response.data; + expect(b.name).toMatch(/_file.html/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/); + response = await request({ url: b.url }); + const body = response.text; + try { + expect(response.headers['content-type']).toMatch('^text/html'); + expect(body).toEqual('\n'); + } catch (e) { + jfail(e); + } }); it('works without Content-Type', done => { @@ -351,25 +354,28 @@ describe('Parse.File testing', () => { ok(object.toJSON().file.url); }); - it('content-type used with no extension', done => { + it('content-type used with no extension', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['*'], + }, + }); const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', }; - request({ + let response = await request({ method: 'POST', headers: headers, url: 'http://localhost:8378/1/files/file', body: 'fee fi fo', - }).then(response => { - const b = response.data; - expect(b.name).toMatch(/\.html$/); - request({ url: b.url }).then(response => { - expect(response.headers['content-type']).toMatch(/^text\/html/); - done(); - }); }); + const b = response.data; + expect(b.name).toMatch(/\.html$/); + response = await request({ url: b.url }); + expect(response.headers['content-type']).toMatch(/^text\/html/); }); it('filename is url encoded', done => { @@ -1298,6 +1304,136 @@ describe('Parse.File testing', () => { await expectAsync(reconfigureServer({ fileUpload: { [key]: value } })).toBeResolved(); } } + await expectAsync( + reconfigureServer({ + fileUpload: { + fileExtensions: 1, + }, + }) + ).toBeRejectedWith('fileUpload.fileExtensions must be an array.'); + }); + }); + + describe('fileExtensions', () => { + it('works with _ContentType', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['png'], + }, + }); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/files/file', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: 'text/html', + base64: 'PGh0bWw+PC9odG1sPgo=', + }), + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) + ); + }); + + it('works without Content-Type', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + }, + }); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + await expectAsync( + request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/file.html', + body: '\n', + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) + ); + }); + + it('works with array', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['jpg'], + }, + }); + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/files/file', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: 'text/html', + base64: 'PGh0bWw+PC9odG1sPgo=', + }), + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) + ); + }); + + it('works with array without Content-Type', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['jpg'], + }, + }); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + await expectAsync( + request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/files/file.html', + body: '\n', + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`) + ); + }); + + it('works with array with correct file type', async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + fileExtensions: ['html'], + }, + }); + const response = await request({ + method: 'POST', + url: 'http://localhost:8378/1/files/file', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: 'text/html', + base64: 'PGh0bWw+PC9odG1sPgo=', + }), + }); + const b = response.data; + expect(b.name).toMatch(/_file.html$/); + expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.html$/); }); }); }); diff --git a/src/Config.js b/src/Config.js index 812d28c3..1cd941ef 100644 --- a/src/Config.js +++ b/src/Config.js @@ -460,6 +460,11 @@ export class Config { } else if (typeof fileUpload.enableForAuthenticatedUser !== 'boolean') { throw 'fileUpload.enableForAuthenticatedUser must be a boolean value.'; } + if (fileUpload.fileExtensions === undefined) { + fileUpload.fileExtensions = FileUploadOptions.fileExtensions.default; + } else if (!Array.isArray(fileUpload.fileExtensions)) { + throw 'fileUpload.fileExtensions must be an array.'; + } } static validateIps(field, masterKeyIps) { diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index 7987363f..85dbeaa8 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -969,6 +969,13 @@ module.exports.FileUploadOptions = { action: parsers.booleanParser, default: false, }, + 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 `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files.", + action: parsers.arrayParser, + default: ['^[^hH][^tT][^mM][^lL]?$'], + }, }; module.exports.DatabaseOptions = { enableSchemaHooks: { diff --git a/src/Options/docs.js b/src/Options/docs.js index b5a78aac..3b48bc2a 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -221,6 +221,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 `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 009b31a5..fc4b269f 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -537,6 +537,9 @@ 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 `^[^hH][^tT][^mM][^lL]?$` which allows any file extension except HTML files. + :DEFAULT: ["^[^hH][^tT][^mM][^lL]?$"] */ + fileExtensions: ?(string[]); /* Is true if file upload should be allowed for anonymous users. :DEFAULT: false */ enableForAnonymousUser: ?boolean; diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index e911d772..ed48a28a 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -140,6 +140,38 @@ export class FilesRouter { return; } + const fileExtensions = config.fileUpload?.fileExtensions; + if (!isMaster && fileExtensions) { + const isValidExtension = extension => { + return fileExtensions.some(ext => { + if (ext === '*') { + return true; + } + const regex = new RegExp(fileExtensions); + if (regex.test(extension)) { + return true; + } + }); + }; + let extension = contentType; + if (filename && filename.includes('.')) { + extension = filename.split('.')[1]; + } else if (contentType && contentType.includes('/')) { + extension = contentType.split('/')[1]; + } + extension = extension.split(' ').join(''); + + if (!isValidExtension(extension)) { + next( + new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `File upload of extension ${extension} is disabled.` + ) + ); + return; + } + } + const base64 = req.body.toString('base64'); const file = new Parse.File(filename, { base64 }, contentType); const { metadata = {}, tags = {} } = req.fileData || {}; From 150627328fd510062f0552a2a8828314b66dc258 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 20 May 2023 23:24:03 +0000 Subject: [PATCH 3/3] chore(release): 6.2.0 [skip ci] # [6.2.0](https://github.com/parse-community/parse-server/compare/6.1.0...6.2.0) (2023-05-20) ### Features * Add new Parse Server option `fileUpload.fileExtensions` to restrict file upload by file extension; this fixes a security vulnerability in which a phishing attack could be performed using an uploaded HTML file; by default the new option only allows file extensions matching the regex pattern `^[^hH][^tT][^mM][^lL]?$`, which excludes HTML files; if your app currently depends on uploading files with HTML file extensions then this may be a breaking change and you could allow HTML file upload by setting the option to `['.*']` ([#8538](https://github.com/parse-community/parse-server/issues/8538)) ([a318e7b](https://github.com/parse-community/parse-server/commit/a318e7bbafcf7a3425b0a1b3c2dd30f526b4b6f9)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 4196602b..4cd0131c 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +# [6.2.0](https://github.com/parse-community/parse-server/compare/6.1.0...6.2.0) (2023-05-20) + + +### Features + +* Add new Parse Server option `fileUpload.fileExtensions` to restrict file upload by file extension; this fixes a security vulnerability in which a phishing attack could be performed using an uploaded HTML file; by default the new option only allows file extensions matching the regex pattern `^[^hH][^tT][^mM][^lL]?$`, which excludes HTML files; if your app currently depends on uploading files with HTML file extensions then this may be a breaking change and you could allow HTML file upload by setting the option to `['.*']` ([#8538](https://github.com/parse-community/parse-server/issues/8538)) ([a318e7b](https://github.com/parse-community/parse-server/commit/a318e7bbafcf7a3425b0a1b3c2dd30f526b4b6f9)) + # [6.1.0](https://github.com/parse-community/parse-server/compare/6.0.0...6.1.0) (2023-05-01) diff --git a/package-lock.json b/package-lock.json index 9445d96f..c1d20e3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "parse-server", - "version": "6.1.0", + "version": "6.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "parse-server", - "version": "6.1.0", + "version": "6.2.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 6981537b..16ae61cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "6.1.0", + "version": "6.2.0", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": {