From c7599c577a02b97eb5e76d4e20517b0283ae73c8 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 13 Mar 2026 21:34:50 +0100 Subject: [PATCH] fix: Stored XSS filter bypass via Content-Type MIME parameter and missing XML extension blocklist entries ([GHSA-42ph-pf9q-cr72](https://github.com/parse-community/parse-server/security/advisories/GHSA-42ph-pf9q-cr72)) (#10192) --- spec/vulnerabilities.spec.js | 87 ++++++++++++++++++++++++++++++++++++ src/Options/Definitions.js | 4 +- src/Options/docs.js | 2 +- src/Options/index.js | 4 +- src/Routers/FilesRouter.js | 3 +- 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/spec/vulnerabilities.spec.js b/spec/vulnerabilities.spec.js index 66f16e85c..50ac46be3 100644 --- a/spec/vulnerabilities.spec.js +++ b/spec/vulnerabilities.spec.js @@ -2424,3 +2424,90 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na }); }); }); + +describe('(GHSA-42ph-pf9q-cr72) Stored XSS filter bypass via parameterized Content-Type and additional XML extensions', () => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + beforeEach(async () => { + await reconfigureServer({ + fileUpload: { + enableForPublic: true, + }, + }); + }); + + for (const { ext, contentType } of [ + { ext: 'xsd', contentType: 'application/xml' }, + { ext: 'rng', contentType: 'application/xml' }, + { ext: 'rdf', contentType: 'application/rdf+xml' }, + { ext: 'owl', contentType: 'application/rdf+xml' }, + { ext: 'mathml', contentType: 'application/mathml+xml' }, + ]) { + it(`blocks .${ext} file upload by default`, async () => { + const content = Buffer.from( + '' + ).toString('base64'); + for (const extension of [ext, ext.toUpperCase(), ext[0].toUpperCase() + ext.slice(1)]) { + await expectAsync( + request({ + method: 'POST', + headers, + url: `http://localhost:8378/1/files/malicious.${extension}`, + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: contentType, + base64: content, + }), + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.FILE_SAVE_ERROR, + `File upload of extension ${extension} is disabled.` + ) + ); + } + }); + } + + it('blocks extensionless upload with parameterized Content-Type that bypasses regex', async () => { + const content = Buffer.from( + '' + ).toString('base64'); + // MIME parameters like ;charset=utf-8 should not bypass the extension filter + const dangerousContentTypes = [ + 'application/xhtml+xml;charset=utf-8', + 'application/xhtml+xml; charset=utf-8', + 'application/xhtml+xml\t;charset=utf-8', + 'image/svg+xml;charset=utf-8', + 'application/xml;charset=utf-8', + 'text/html;charset=utf-8', + 'application/xslt+xml;charset=utf-8', + 'application/rdf+xml;charset=utf-8', + 'application/mathml+xml;charset=utf-8', + ]; + for (const contentType of dangerousContentTypes) { + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/files/payload', + body: JSON.stringify({ + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ContentType: contentType, + base64: content, + }), + }).catch(e => { + throw new Error(e.data.error); + }) + ).toBeRejectedWith(jasmine.objectContaining({ + message: jasmine.stringMatching(/File upload of extension .+ is disabled/), + })); + } + }); +}); diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index df6a7ef36..d0cf5f8ff 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -1145,10 +1145,10 @@ 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, SVG, and XML files are especially problematic as they may be used by an attacker who uploads a HTML form, SVG image, or XML document 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]?(\\\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\\\+[xX][mM][lL])?)$)` which allows any file extension except those that are rendered as website or active content by a web browser.", + '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 only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.

The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser\'s local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.

Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\\\+[xX][mM][lL])?)$)"]`.', action: parsers.arrayParser, default: [ - '^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?)$)', + '^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)', ], }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index ba02a4aa5..20a6f95ab 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -248,7 +248,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, SVG, and XML files are especially problematic as they may be used by an attacker who uploads a HTML form, SVG image, or XML document 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]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?)$)` which allows any file extension except those that are rendered as website or active content by a web browser. + * @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 only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.

The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser's local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.

Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)"]`. */ /** diff --git a/src/Options/index.js b/src/Options/index.js index 8080914ee..fa8ba73b6 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -630,8 +630,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, SVG, and XML files are especially problematic as they may be used by an attacker who uploads a HTML form, SVG image, or XML document 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]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?)$)` which allows any file extension except those that are rendered as website or active content by a web browser. - :DEFAULT: ["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?)$)"] */ + /* 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 only allow the file extensions that your app actually needs, rather than relying on blocking dangerous extensions. This allowlist approach is more secure because new dangerous file extensions may emerge that are not covered by the default blocklist.

The default blocks the most common file extensions that are known to be rendered as active content by web browsers, such as HTML, SVG, and XML files, which may be used by an attacker to compromise the session token of another user via accessing the browser's local storage. The blocked extensions are: `html`, `htm`, `shtml`, `xhtml`, `xhtml+xml`, `xht`, `svg`, `svgz`, `svg+xml`, `xml`, `xsl`, `xslt`, `xslt+xml`, `xsd`, `rng`, `rdf`, `rdf+xml`, `owl`, `mathml`, `mathml+xml`.

Defaults to `["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][mM][lL])?)$)"]`. + :DEFAULT: ["^(?!([xXsS]?[hH][tT][mM][lL]?(\\+[xX][mM][lL])?|[xX][hH][tT]|[sS][vV][gG]([zZ]|\\+[xX][mM][lL])?|[xX][mM][lL]|[xX][sS][lL][tT]?(\\+[xX][mM][lL])?|[xX][sS][dD]|[rR][nN][gG]|[rR][dD][fF](\\+[xX][mM][lL])?|[oO][wW][lL]|[mM][aA][tT][hH][mM][lL](\\+[xX][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 8a262e51c..0f4dbfda8 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -176,7 +176,8 @@ export class FilesRouter { } else if (contentType && contentType.includes('/')) { extension = contentType.split('/')[1]; } - extension = extension?.split(' ')?.join(''); + // Strip MIME parameters (e.g. ";charset=utf-8") and whitespace + extension = extension?.split(';')[0]?.replace(/\s+/g, ''); if (extension && !isValidExtension(extension)) { next(