Skip to content

Commit dd7cc41

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

File tree

2 files changed

+84
-2
lines changed

2 files changed

+84
-2
lines changed

spec/vulnerabilities.spec.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,88 @@ describe('Vulnerabilities', () => {
16631663
});
16641664
});
16651665

1666+
describe('(GHSA-vr5f-2r24-w5hc) Stored XSS via Content-Type and file extension mismatch', () => {
1667+
const headers = {
1668+
'X-Parse-Application-Id': 'test',
1669+
'X-Parse-REST-API-Key': 'rest',
1670+
};
1671+
1672+
it('overrides mismatched Content-Type with extension-derived MIME type on buffered upload', async () => {
1673+
const adapter = Config.get('test').filesController.adapter;
1674+
const spy = spyOn(adapter, 'createFile').and.callThrough();
1675+
const content = Buffer.from('<script>alert(1)</script>').toString('base64');
1676+
await request({
1677+
method: 'POST',
1678+
url: 'http://localhost:8378/1/files/evil.txt',
1679+
body: JSON.stringify({
1680+
_ApplicationId: 'test',
1681+
_JavaScriptKey: 'test',
1682+
_ContentType: 'text/html',
1683+
base64: content,
1684+
}),
1685+
headers,
1686+
});
1687+
expect(spy).toHaveBeenCalled();
1688+
const contentTypeArg = spy.calls.mostRecent().args[2];
1689+
expect(contentTypeArg).toBe('text/plain');
1690+
});
1691+
1692+
it('overrides mismatched Content-Type with extension-derived MIME type on stream upload', async () => {
1693+
const adapter = Config.get('test').filesController.adapter;
1694+
const spy = spyOn(adapter, 'createFile').and.callThrough();
1695+
const body = '<script>alert(1)</script>';
1696+
await request({
1697+
method: 'POST',
1698+
url: 'http://localhost:8378/1/files/evil.txt',
1699+
headers: {
1700+
...headers,
1701+
'Content-Type': 'text/html',
1702+
'X-Parse-Upload-Mode': 'stream',
1703+
},
1704+
body,
1705+
});
1706+
expect(spy).toHaveBeenCalled();
1707+
const contentTypeArg = spy.calls.mostRecent().args[2];
1708+
expect(contentTypeArg).toBe('text/plain');
1709+
});
1710+
1711+
it('preserves Content-Type when no file extension is present', async () => {
1712+
const adapter = Config.get('test').filesController.adapter;
1713+
const spy = spyOn(adapter, 'createFile').and.callThrough();
1714+
await request({
1715+
method: 'POST',
1716+
url: 'http://localhost:8378/1/files/noextension',
1717+
headers: {
1718+
...headers,
1719+
'Content-Type': 'image/png',
1720+
},
1721+
body: Buffer.from('fake png content'),
1722+
});
1723+
expect(spy).toHaveBeenCalled();
1724+
const contentTypeArg = spy.calls.mostRecent().args[2];
1725+
expect(contentTypeArg).toBe('image/png');
1726+
});
1727+
1728+
it('infers Content-Type from extension when none is provided', async () => {
1729+
const adapter = Config.get('test').filesController.adapter;
1730+
const spy = spyOn(adapter, 'createFile').and.callThrough();
1731+
const content = Buffer.from('test content').toString('base64');
1732+
await request({
1733+
method: 'POST',
1734+
url: 'http://localhost:8378/1/files/data.txt',
1735+
body: JSON.stringify({
1736+
_ApplicationId: 'test',
1737+
_JavaScriptKey: 'test',
1738+
base64: content,
1739+
}),
1740+
headers,
1741+
});
1742+
expect(spy).toHaveBeenCalled();
1743+
const contentTypeArg = spy.calls.mostRecent().args[2];
1744+
expect(contentTypeArg).toBe('text/plain');
1745+
});
1746+
});
1747+
16661748
describe('(GHSA-q3vj-96h2-gwvg) SQL Injection via Increment amount on nested Object field', () => {
16671749
const headers = {
16681750
'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)