fix: Stored XSS via file upload of HTML-renderable file types ([GHSA-v5hf-f4c3-m5rv](https://github.com/parse-community/parse-server/security/advisories/GHSA-v5hf-f4c3-m5rv)) (#10162)

This commit is contained in:
Manuel
2026-03-09 23:50:23 +00:00
committed by GitHub
parent 10547a64fa
commit 03287cf83b
4 changed files with 222 additions and 5 deletions
+215
View File
@@ -1021,6 +1021,221 @@ describe('(GHSA-qpr4-jrj4-6f27) SQL Injection via sort dot-notation field name',
});
});
describe('(GHSA-v5hf-f4c3-m5rv) Stored XSS via .svgz, .xht, .xml, .xsl, .xslt file upload', () => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
beforeEach(async () => {
await reconfigureServer({
fileUpload: {
enableForPublic: true,
},
});
});
it('blocks .svgz file upload by default', async () => {
const svgContent = Buffer.from(
'<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>'
).toString('base64');
for (const extension of ['svgz', 'SVGZ', 'Svgz']) {
await expectAsync(
request({
method: 'POST',
headers,
url: `http://localhost:8378/1/files/malicious.${extension}`,
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'image/svg+xml',
base64: svgContent,
}),
}).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 .xht file upload by default', async () => {
const xhtContent = Buffer.from(
'<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><body><script>alert(1)</script></body></html>'
).toString('base64');
for (const extension of ['xht', 'XHT', 'Xht']) {
await expectAsync(
request({
method: 'POST',
headers,
url: `http://localhost:8378/1/files/malicious.${extension}`,
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'application/xhtml+xml',
base64: xhtContent,
}),
}).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 .xml file upload by default', async () => {
const xmlContent = Buffer.from(
'<?xml version="1.0"?><root><data>test</data></root>'
).toString('base64');
for (const extension of ['xml', 'XML', 'Xml']) {
await expectAsync(
request({
method: 'POST',
headers,
url: `http://localhost:8378/1/files/malicious.${extension}`,
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'application/xml',
base64: xmlContent,
}),
}).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 .xsl file upload by default', async () => {
const xslContent = Buffer.from(
'<?xml version="1.0"?><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"></xsl:stylesheet>'
).toString('base64');
for (const extension of ['xsl', 'XSL', 'Xsl']) {
await expectAsync(
request({
method: 'POST',
headers,
url: `http://localhost:8378/1/files/malicious.${extension}`,
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'application/xml',
base64: xslContent,
}),
}).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 .xslt file upload by default', async () => {
const xsltContent = Buffer.from(
'<?xml version="1.0"?><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"></xsl:stylesheet>'
).toString('base64');
for (const extension of ['xslt', 'XSLT', 'Xslt']) {
await expectAsync(
request({
method: 'POST',
headers,
url: `http://localhost:8378/1/files/malicious.${extension}`,
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'application/xslt+xml',
base64: xsltContent,
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
`File upload of extension ${extension} is disabled.`
)
);
}
});
// Headers are intentionally omitted below so that the middleware parses _ContentType
// from the JSON body and sets it as the content-type header. When X-Parse-Application-Id
// is sent as a header, the middleware skips body parsing and _ContentType is ignored.
it('blocks extensionless upload with application/xhtml+xml content type', async () => {
const xhtContent = Buffer.from(
'<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><body><script>alert(1)</script></body></html>'
).toString('base64');
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/payload',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'application/xhtml+xml',
base64: xhtContent,
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'File upload of extension xhtml+xml is disabled.'
)
);
});
it('blocks extensionless upload with application/xslt+xml content type', async () => {
const xsltContent = Buffer.from(
'<?xml version="1.0"?><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"></xsl:stylesheet>'
).toString('base64');
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/files/payload',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'application/xslt+xml',
base64: xsltContent,
}),
}).catch(e => {
throw new Error(e.data.error);
})
).toBeRejectedWith(
new Parse.Error(
Parse.Error.FILE_SAVE_ERROR,
'File upload of extension xslt+xml is disabled.'
)
);
});
it('still allows common file types', async () => {
for (const type of ['txt', 'png', 'jpg', 'gif', 'pdf', 'doc']) {
const file = new Parse.File(`file.${type}`, { base64: 'ParseA==' });
await file.save();
}
});
});
describe('(GHSA-3jmq-rrxf-gqrg) Stored XSS via file serving', () => {
it('sets X-Content-Type-Options: nosniff on file GET response', async () => {
const file = new Parse.File('hello.txt', [1, 2, 3], 'text/plain');
+4 -2
View File
@@ -1059,9 +1059,11 @@ 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 and SVG files are especially problematic as they may be used by an attacker who uploads a HTML form or SVG image 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]?|[sS][vV][gG](\\\\+[xX][mM][lL])?)$)` which allows any file extension except those that are rendered as website or active content by a web browser.",
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, 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.",
action: parsers.arrayParser,
default: ['^(?!([xXsS]?[hH][tT][mM][lL]?|[sS][vV][gG](\\+[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])?)$)',
],
},
};
/* The available log levels for Parse Server logging. Valid values are:<br>- `'error'` - Error level (highest priority)<br>- `'warn'` - Warning level<br>- `'info'` - Info level (default)<br>- `'verbose'` - Verbose level<br>- `'debug'` - Debug level<br>- `'silly'` - Silly level (lowest priority) */
+1 -1
View File
@@ -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.<br><br>It is recommended to restrict the file upload extensions as much as possible. HTML and SVG files are especially problematic as they may be used by an attacker who uploads a HTML form or SVG image 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]?|[sS][vV][gG](\\+[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.<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.
*/
/**
+2 -2
View File
@@ -648,8 +648,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 and SVG files are especially problematic as they may be used by an attacker who uploads a HTML form or SVG image 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]?|[sS][vV][gG](\\+[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]?|[sS][vV][gG](\\+[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.<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.
: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])?)$)"] */
fileExtensions: ?(string[]);
/* Is true if file upload should be allowed for anonymous users.
:DEFAULT: false */