Skip to content
74 changes: 74 additions & 0 deletions spec/ParseFile.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,80 @@ describe('Parse.File testing', () => {
done();
});
});

describe('URI-backed file upload is disabled to prevent SSRF attack', () => {
const express = require('express');
let testServer;
let testServerPort;
let requestsMade;

beforeEach(async () => {
requestsMade = [];
const app = express();
app.use((req, res) => {
requestsMade.push({ url: req.url, method: req.method });
res.status(200).send('test file content');
});
testServer = app.listen(0);
testServerPort = testServer.address().port;
});

afterEach(async () => {
if (testServer) {
await new Promise(resolve => testServer.close(resolve));
}
Parse.Cloud._removeAllHooks();
});

it('does not access URI when file upload attempted over REST', async () => {
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestClass',
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
body: {
file: {
__type: 'File',
name: 'test.txt',
_source: {
format: 'uri',
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
},
},
},
});
expect(response.status).toBe(201);
// Verify no HTTP request was made to the URI
expect(requestsMade.length).toBe(0);
});

it('does not access URI when file created in beforeSave trigger', async () => {
Parse.Cloud.beforeSave(Parse.File, () => {
return new Parse.File('trigger-file.txt', {
uri: `http://127.0.0.1:${testServerPort}/secret-file.txt`,
});
});
await expectAsync(
request({
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
url: 'http://localhost:8378/1/files/test.txt',
body: 'test content',
})
).toBeRejectedWith(jasmine.objectContaining({
status: 400
}));
// Verify no HTTP request was made to the URI
expect(requestsMade.length).toBe(0);
});
});
});

describe('deleting files', () => {
Expand Down
28 changes: 0 additions & 28 deletions src/Routers/FilesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,8 @@ import Parse from 'parse/node';
import Config from '../Config';
import logger from '../logger';
const triggers = require('../triggers');
const http = require('http');
const Utils = require('../Utils');

const downloadFileFromURI = uri => {
return new Promise((res, rej) => {
http
.get(uri, response => {
response.setDefaultEncoding('base64');
let body = `data:${response.headers['content-type']};base64,`;
response.on('data', data => (body += data));
response.on('end', () => res(body));
})
.on('error', e => {
rej(`Error downloading file from ${uri}: ${e.message}`);
});
});
};

const addFileDataIfNeeded = async file => {
if (file._source.format === 'uri') {
const base64 = await downloadFileFromURI(file._source.uri);
file._previousSave = file;
file._data = base64;
file._requestTask = null;
}
return file;
};

export class FilesRouter {
expressRouter({ maxUploadSize = '20Mb' } = {}) {
var router = express.Router();
Expand Down Expand Up @@ -247,8 +221,6 @@ export class FilesRouter {
}
// if the file returned by the trigger has already been saved skip saving anything
if (!saveResult) {
// if the ParseFile returned is type uri, download the file before saving it
await addFileDataIfNeeded(fileObject.file);
// update fileSize
const bufferData = Buffer.from(fileObject.file._data, 'base64');
fileObject.fileSize = Buffer.byteLength(bufferData);
Expand Down
Loading