Skip to content

Commit 8bbe3ef

Browse files
authored
fix: Uploading a file by providing an origin URL allows for Server-Side Request Forgery (SSRF); fixes vulnerability [GHSA-x4qj-2f4q-r4rx](GHSA-x4qj-2f4q-r4rx) (#9904)
1 parent 6096449 commit 8bbe3ef

File tree

2 files changed

+74
-28
lines changed

2 files changed

+74
-28
lines changed

spec/ParseFile.spec.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,80 @@ describe('Parse.File testing', () => {
633633
done();
634634
});
635635
});
636+
637+
describe('URI-backed file upload is disabled to prevent SSRF attack', () => {
638+
const express = require('express');
639+
let testServer;
640+
let testServerPort;
641+
let requestsMade;
642+
643+
beforeEach(async () => {
644+
requestsMade = [];
645+
const app = express();
646+
app.use((req, res) => {
647+
requestsMade.push({ url: req.url, method: req.method });
648+
res.status(200).send('test file content');
649+
});
650+
testServer = app.listen(0);
651+
testServerPort = testServer.address().port;
652+
});
653+
654+
afterEach(async () => {
655+
if (testServer) {
656+
await new Promise(resolve => testServer.close(resolve));
657+
}
658+
Parse.Cloud._removeAllHooks();
659+
});
660+
661+
it('does not access URI when file upload attempted over REST', async () => {
662+
const response = await request({
663+
method: 'POST',
664+
url: 'http://localhost:8378/1/classes/TestClass',
665+
headers: {
666+
'Content-Type': 'application/json',
667+
'X-Parse-Application-Id': 'test',
668+
'X-Parse-REST-API-Key': 'rest',
669+
},
670+
body: {
671+
file: {
672+
__type: 'File',
673+
name: 'test.txt',
674+
_source: {
675+
format: 'uri',
676+
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
677+
},
678+
},
679+
},
680+
});
681+
expect(response.status).toBe(201);
682+
// Verify no HTTP request was made to the URI
683+
expect(requestsMade.length).toBe(0);
684+
});
685+
686+
it('does not access URI when file created in beforeSave trigger', async () => {
687+
Parse.Cloud.beforeSave(Parse.File, () => {
688+
return new Parse.File('trigger-file.txt', {
689+
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
690+
});
691+
});
692+
await expectAsync(
693+
request({
694+
method: 'POST',
695+
headers: {
696+
'Content-Type': 'application/octet-stream',
697+
'X-Parse-Application-Id': 'test',
698+
'X-Parse-REST-API-Key': 'rest',
699+
},
700+
url: 'http://localhost:8378/1/files/test.txt',
701+
body: 'test content',
702+
})
703+
).toBeRejectedWith(jasmine.objectContaining({
704+
status: 400
705+
}));
706+
// Verify no HTTP request was made to the URI
707+
expect(requestsMade.length).toBe(0);
708+
});
709+
});
636710
});
637711

638712
describe('deleting files', () => {

src/Routers/FilesRouter.js

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,8 @@ import Parse from 'parse/node';
55
import Config from '../Config';
66
import logger from '../logger';
77
const triggers = require('../triggers');
8-
const http = require('http');
98
const Utils = require('../Utils');
109

11-
const downloadFileFromURI = uri => {
12-
return new Promise((res, rej) => {
13-
http
14-
.get(uri, response => {
15-
response.setDefaultEncoding('base64');
16-
let body = `data:${response.headers['content-type']};base64,`;
17-
response.on('data', data => (body += data));
18-
response.on('end', () => res(body));
19-
})
20-
.on('error', e => {
21-
rej(`Error downloading file from ${uri}: ${e.message}`);
22-
});
23-
});
24-
};
25-
26-
const addFileDataIfNeeded = async file => {
27-
if (file._source.format === 'uri') {
28-
const base64 = await downloadFileFromURI(file._source.uri);
29-
file._previousSave = file;
30-
file._data = base64;
31-
file._requestTask = null;
32-
}
33-
return file;
34-
};
35-
3610
export class FilesRouter {
3711
expressRouter({ maxUploadSize = '20Mb' } = {}) {
3812
var router = express.Router();
@@ -210,8 +184,6 @@ export class FilesRouter {
210184
}
211185
// if the file returned by the trigger has already been saved skip saving anything
212186
if (!saveResult) {
213-
// if the ParseFile returned is type uri, download the file before saving it
214-
await addFileDataIfNeeded(fileObject.file);
215187
// update fileSize
216188
const bufferData = Buffer.from(fileObject.file._data, 'base64');
217189
fileObject.fileSize = Buffer.byteLength(bufferData);

0 commit comments

Comments
 (0)