Skip to content

Commit c7599c5

Browse files
authored
fix: Stored XSS filter bypass via Content-Type MIME parameter and missing XML extension blocklist entries ([GHSA-42ph-pf9q-cr72](GHSA-42ph-pf9q-cr72)) (#10192)
1 parent e90db39 commit c7599c5

File tree

5 files changed

+94
-6
lines changed

5 files changed

+94
-6
lines changed

spec/vulnerabilities.spec.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,3 +2424,90 @@ describe('(GHSA-c442-97qw-j6c6) SQL Injection via $regex query operator field na
24242424
});
24252425
});
24262426
});
2427+
2428+
describe('(GHSA-42ph-pf9q-cr72) Stored XSS filter bypass via parameterized Content-Type and additional XML extensions', () => {
2429+
const headers = {
2430+
'X-Parse-Application-Id': 'test',
2431+
'X-Parse-REST-API-Key': 'rest',
2432+
};
2433+
2434+
beforeEach(async () => {
2435+
await reconfigureServer({
2436+
fileUpload: {
2437+
enableForPublic: true,
2438+
},
2439+
});
2440+
});
2441+
2442+
for (const { ext, contentType } of [
2443+
{ ext: 'xsd', contentType: 'application/xml' },
2444+
{ ext: 'rng', contentType: 'application/xml' },
2445+
{ ext: 'rdf', contentType: 'application/rdf+xml' },
2446+
{ ext: 'owl', contentType: 'application/rdf+xml' },
2447+
{ ext: 'mathml', contentType: 'application/mathml+xml' },
2448+
]) {
2449+
it(`blocks .${ext} file upload by default`, async () => {
2450+
const content = Buffer.from(
2451+
'<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><body><script>alert(1)</script></body></html>'
2452+
).toString('base64');
2453+
for (const extension of [ext, ext.toUpperCase(), ext[0].toUpperCase() + ext.slice(1)]) {
2454+
await expectAsync(
2455+
request({
2456+
method: 'POST',
2457+
headers,
2458+
url: `http://localhost:8378/1/files/malicious.${extension}`,
2459+
body: JSON.stringify({
2460+
_ApplicationId: 'test',
2461+
_JavaScriptKey: 'test',
2462+
_ContentType: contentType,
2463+
base64: content,
2464+
}),
2465+
}).catch(e => {
2466+
throw new Error(e.data.error);
2467+
})
2468+
).toBeRejectedWith(
2469+
new Parse.Error(
2470+
Parse.Error.FILE_SAVE_ERROR,
2471+
`File upload of extension ${extension} is disabled.`
2472+
)
2473+
);
2474+
}
2475+
});
2476+
}
2477+
2478+
it('blocks extensionless upload with parameterized Content-Type that bypasses regex', async () => {
2479+
const content = Buffer.from(
2480+
'<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><body><script>alert(1)</script></body></html>'
2481+
).toString('base64');
2482+
// MIME parameters like ;charset=utf-8 should not bypass the extension filter
2483+
const dangerousContentTypes = [
2484+
'application/xhtml+xml;charset=utf-8',
2485+
'application/xhtml+xml; charset=utf-8',
2486+
'application/xhtml+xml\t;charset=utf-8',
2487+
'image/svg+xml;charset=utf-8',
2488+
'application/xml;charset=utf-8',
2489+
'text/html;charset=utf-8',
2490+
'application/xslt+xml;charset=utf-8',
2491+
'application/rdf+xml;charset=utf-8',
2492+
'application/mathml+xml;charset=utf-8',
2493+
];
2494+
for (const contentType of dangerousContentTypes) {
2495+
await expectAsync(
2496+
request({
2497+
method: 'POST',
2498+
url: 'http://localhost:8378/1/files/payload',
2499+
body: JSON.stringify({
2500+
_ApplicationId: 'test',
2501+
_JavaScriptKey: 'test',
2502+
_ContentType: contentType,
2503+
base64: content,
2504+
}),
2505+
}).catch(e => {
2506+
throw new Error(e.data.error);
2507+
})
2508+
).toBeRejectedWith(jasmine.objectContaining({
2509+
message: jasmine.stringMatching(/File upload of extension .+ is disabled/),
2510+
}));
2511+
}
2512+
});
2513+
});

src/Options/Definitions.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,10 +1145,10 @@ module.exports.FileUploadOptions = {
11451145
fileExtensions: {
11461146
env: 'PARSE_SERVER_FILE_UPLOAD_FILE_EXTENSIONS',
11471147
help:
1148-
"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, 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.<br><br>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.",
1148+
'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 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.<br><br>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`.<br><br>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])?)$)"]`.',
11491149
action: parsers.arrayParser,
11501150
default: [
1151-
'^(?!([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])?)$)',
1151+
'^(?!([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])?)$)',
11521152
],
11531153
},
11541154
};

src/Options/docs.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Options/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -630,8 +630,8 @@ export interface PasswordPolicyOptions {
630630
}
631631

632632
export interface FileUploadOptions {
633-
/* 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, 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.<br><br>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.
634-
: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])?)$)"] */
633+
/* 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 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.<br><br>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`.<br><br>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])?)$)"]`.
634+
: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])?)$)"] */
635635
fileExtensions: ?(string[]);
636636
/* Is true if file upload should be allowed for anonymous users.
637637
:DEFAULT: false */

src/Routers/FilesRouter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ export class FilesRouter {
176176
} else if (contentType && contentType.includes('/')) {
177177
extension = contentType.split('/')[1];
178178
}
179-
extension = extension?.split(' ')?.join('');
179+
// Strip MIME parameters (e.g. ";charset=utf-8") and whitespace
180+
extension = extension?.split(';')[0]?.replace(/\s+/g, '');
180181

181182
if (extension && !isValidExtension(extension)) {
182183
next(

0 commit comments

Comments
 (0)