diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 6be506be8d..d6539b7336 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -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', () => { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 0bec64c9aa..5cb39abf47 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -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(); @@ -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);