Skip to content

Commit 0de3e9f

Browse files
authored
fix: File upload Content-Type override via extension mismatch ([GHSA-vr5f-2r24-w5hc](GHSA-vr5f-2r24-w5hc)) (#10384)
1 parent 8191b6d commit 0de3e9f

File tree

2 files changed

+65
-2
lines changed

2 files changed

+65
-2
lines changed

spec/vulnerabilities.spec.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2769,6 +2769,69 @@ describe('Vulnerabilities', () => {
27692769
}
27702770
});
27712771

2772+
describe('(GHSA-vr5f-2r24-w5hc) Stored XSS via Content-Type and file extension mismatch', () => {
2773+
const headers = {
2774+
'X-Parse-Application-Id': 'test',
2775+
'X-Parse-REST-API-Key': 'rest',
2776+
};
2777+
2778+
it('overrides mismatched Content-Type with extension-derived MIME type on buffered upload', async () => {
2779+
const adapter = Config.get('test').filesController.adapter;
2780+
const spy = spyOn(adapter, 'createFile').and.callThrough();
2781+
const content = Buffer.from('<script>alert(1)</script>').toString('base64');
2782+
await request({
2783+
method: 'POST',
2784+
url: 'http://localhost:8378/1/files/evil.txt',
2785+
body: JSON.stringify({
2786+
_ApplicationId: 'test',
2787+
_JavaScriptKey: 'test',
2788+
_ContentType: 'text/html',
2789+
base64: content,
2790+
}),
2791+
headers,
2792+
});
2793+
expect(spy).toHaveBeenCalled();
2794+
const contentTypeArg = spy.calls.mostRecent().args[2];
2795+
expect(contentTypeArg).toBe('text/plain');
2796+
});
2797+
2798+
it('preserves Content-Type when no file extension is present', async () => {
2799+
const adapter = Config.get('test').filesController.adapter;
2800+
const spy = spyOn(adapter, 'createFile').and.callThrough();
2801+
await request({
2802+
method: 'POST',
2803+
url: 'http://localhost:8378/1/files/noextension',
2804+
headers: {
2805+
...headers,
2806+
'Content-Type': 'image/png',
2807+
},
2808+
body: Buffer.from('fake png content'),
2809+
});
2810+
expect(spy).toHaveBeenCalled();
2811+
const contentTypeArg = spy.calls.mostRecent().args[2];
2812+
expect(contentTypeArg).toBe('image/png');
2813+
});
2814+
2815+
it('infers Content-Type from extension when none is provided', async () => {
2816+
const adapter = Config.get('test').filesController.adapter;
2817+
const spy = spyOn(adapter, 'createFile').and.callThrough();
2818+
const content = Buffer.from('test content').toString('base64');
2819+
await request({
2820+
method: 'POST',
2821+
url: 'http://localhost:8378/1/files/data.txt',
2822+
body: JSON.stringify({
2823+
_ApplicationId: 'test',
2824+
_JavaScriptKey: 'test',
2825+
base64: content,
2826+
}),
2827+
headers,
2828+
});
2829+
expect(spy).toHaveBeenCalled();
2830+
const contentTypeArg = spy.calls.mostRecent().args[2];
2831+
expect(contentTypeArg).toBe('text/plain');
2832+
});
2833+
});
2834+
27722835
describe('(GHSA-9ccr-fpp6-78qf) Schema poisoning via __proto__ bypassing requestKeywordDenylist and addField CLP', () => {
27732836
const headers = {
27742837
'Content-Type': 'application/json',

src/Controllers/FilesController.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export class FilesController extends AdaptableController {
2121
const mime = (await import('mime')).default
2222
if (!hasExtension && contentType && mime.getExtension(contentType)) {
2323
filename = filename + '.' + mime.getExtension(contentType);
24-
} else if (hasExtension && !contentType) {
25-
contentType = mime.getType(filename);
24+
} else if (hasExtension) {
25+
contentType = mime.getType(filename) || contentType;
2626
}
2727

2828
if (!this.options.preserveFileName) {

0 commit comments

Comments
 (0)