mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
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:
@@ -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');
|
||||
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user