fix: File upload Content-Type override via extension mismatch ([GHSA-vr5f-2r24-w5hc](https://github.com/parse-community/parse-server/security/advisories/GHSA-vr5f-2r24-w5hc)) (#10384)

This commit is contained in:
Manuel
2026-04-02 02:21:08 +01:00
committed by GitHub
parent 8191b6d3e6
commit 0de3e9f4bd
2 changed files with 65 additions and 2 deletions
+63
View File
@@ -2769,6 +2769,69 @@ describe('Vulnerabilities', () => {
}
});
describe('(GHSA-vr5f-2r24-w5hc) Stored XSS via Content-Type and file extension mismatch', () => {
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
it('overrides mismatched Content-Type with extension-derived MIME type on buffered upload', async () => {
const adapter = Config.get('test').filesController.adapter;
const spy = spyOn(adapter, 'createFile').and.callThrough();
const content = Buffer.from('<script>alert(1)</script>').toString('base64');
await request({
method: 'POST',
url: 'http://localhost:8378/1/files/evil.txt',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
_ContentType: 'text/html',
base64: content,
}),
headers,
});
expect(spy).toHaveBeenCalled();
const contentTypeArg = spy.calls.mostRecent().args[2];
expect(contentTypeArg).toBe('text/plain');
});
it('preserves Content-Type when no file extension is present', async () => {
const adapter = Config.get('test').filesController.adapter;
const spy = spyOn(adapter, 'createFile').and.callThrough();
await request({
method: 'POST',
url: 'http://localhost:8378/1/files/noextension',
headers: {
...headers,
'Content-Type': 'image/png',
},
body: Buffer.from('fake png content'),
});
expect(spy).toHaveBeenCalled();
const contentTypeArg = spy.calls.mostRecent().args[2];
expect(contentTypeArg).toBe('image/png');
});
it('infers Content-Type from extension when none is provided', async () => {
const adapter = Config.get('test').filesController.adapter;
const spy = spyOn(adapter, 'createFile').and.callThrough();
const content = Buffer.from('test content').toString('base64');
await request({
method: 'POST',
url: 'http://localhost:8378/1/files/data.txt',
body: JSON.stringify({
_ApplicationId: 'test',
_JavaScriptKey: 'test',
base64: content,
}),
headers,
});
expect(spy).toHaveBeenCalled();
const contentTypeArg = spy.calls.mostRecent().args[2];
expect(contentTypeArg).toBe('text/plain');
});
});
describe('(GHSA-9ccr-fpp6-78qf) Schema poisoning via __proto__ bypassing requestKeywordDenylist and addField CLP', () => {
const headers = {
'Content-Type': 'application/json',
+2 -2
View File
@@ -21,8 +21,8 @@ export class FilesController extends AdaptableController {
const mime = (await import('mime')).default
if (!hasExtension && contentType && mime.getExtension(contentType)) {
filename = filename + '.' + mime.getExtension(contentType);
} else if (hasExtension && !contentType) {
contentType = mime.getType(filename);
} else if (hasExtension) {
contentType = mime.getType(filename) || contentType;
}
if (!this.options.preserveFileName) {