fix: Uploading a file by providing an origin URL allows for Server-Side Request Forgery (SSRF); fixes vulnerability [GHSA-x4qj-2f4q-r4rx](https://github.com/parse-community/parse-server/security/advisories/GHSA-x4qj-2f4q-r4rx) (#9904)
This commit is contained in:
@@ -633,6 +633,80 @@ describe('Parse.File testing', () => {
|
|||||||
done();
|
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', () => {
|
describe('deleting files', () => {
|
||||||
|
|||||||
@@ -5,34 +5,8 @@ import Parse from 'parse/node';
|
|||||||
import Config from '../Config';
|
import Config from '../Config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
const triggers = require('../triggers');
|
const triggers = require('../triggers');
|
||||||
const http = require('http');
|
|
||||||
const Utils = require('../Utils');
|
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 {
|
export class FilesRouter {
|
||||||
expressRouter({ maxUploadSize = '20Mb' } = {}) {
|
expressRouter({ maxUploadSize = '20Mb' } = {}) {
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
@@ -210,8 +184,6 @@ export class FilesRouter {
|
|||||||
}
|
}
|
||||||
// if the file returned by the trigger has already been saved skip saving anything
|
// if the file returned by the trigger has already been saved skip saving anything
|
||||||
if (!saveResult) {
|
if (!saveResult) {
|
||||||
// if the ParseFile returned is type uri, download the file before saving it
|
|
||||||
await addFileDataIfNeeded(fileObject.file);
|
|
||||||
// update fileSize
|
// update fileSize
|
||||||
const bufferData = Buffer.from(fileObject.file._data, 'base64');
|
const bufferData = Buffer.from(fileObject.file._data, 'base64');
|
||||||
fileObject.fileSize = Buffer.byteLength(bufferData);
|
fileObject.fileSize = Buffer.byteLength(bufferData);
|
||||||
|
|||||||
Reference in New Issue
Block a user