fix: server crashes when receiving file download request with invalid byte range; this fixes a security vulnerability that allows an attacker to impact the availability of the server instance; the fix improves parsing of the range parameter to properly handle invalid range requests ([GHSA-h423-w6qv-2wj3](https://github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3)) [skip release] (#8238)

This commit is contained in:
Manuel
2022-10-15 01:06:45 +02:00
committed by GitHub
parent 89fad24bae
commit c03908f74e
3 changed files with 228 additions and 21 deletions

View File

@@ -692,7 +692,198 @@ describe('Parse.File testing', () => {
}); });
}); });
xdescribe('Gridstore Range tests', () => { describe_only_db('mongo')('Gridstore Range', () => {
it('supports bytes range out of range', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=15000-18000',
},
});
expect(file.headers['content-range']).toBe('bytes 1212-1212/1212');
});
it('supports bytes range if end greater than start', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=15000-100',
},
});
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
});
it('supports bytes range if end is undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=100-',
},
});
expect(file.headers['content-range']).toBe('bytes 100-1212/1212');
});
it('supports bytes range if start and end undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=abc-efs',
},
}).catch(e => e);
expect(file.headers['content-range']).toBeUndefined();
});
it('supports bytes range if start and end undefined', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
},
}).catch(e => e);
expect(file.headers['content-range']).toBeUndefined();
});
it('supports bytes range if end is greater than size', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=0-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
});
it('supports bytes range if end is greater than size', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: repeat('argle bargle', 100),
});
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=0-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1212/1212');
});
it('supports bytes range with 0 length', async () => {
const headers = {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
const response = await request({
method: 'POST',
headers: headers,
url: 'http://localhost:8378/1//files/file.txt ',
body: 'a',
}).catch(e => e);
const b = response.data;
const file = await request({
url: b.url,
headers: {
'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test',
Range: 'bytes=-2000',
},
}).catch(e => e);
expect(file.headers['content-range']).toBe('bytes 0-1/1');
});
it('supports range requests', done => { it('supports range requests', done => {
const headers = { const headers = {
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
@@ -781,7 +972,7 @@ describe('Parse.File testing', () => {
}); });
}); });
xit('supports getting last n bytes', done => { it('supports getting last n bytes', done => {
const headers = { const headers = {
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
@@ -879,8 +1070,8 @@ describe('Parse.File testing', () => {
}); });
}); });
it('fails to stream unknown file', done => { it('fails to stream unknown file', async () => {
request({ const response = await request({
url: 'http://localhost:8378/1/files/test/file.txt', url: 'http://localhost:8378/1/files/test/file.txt',
headers: { headers: {
'Content-Type': 'application/octet-stream', 'Content-Type': 'application/octet-stream',
@@ -888,12 +1079,10 @@ describe('Parse.File testing', () => {
'X-Parse-REST-API-Key': 'rest', 'X-Parse-REST-API-Key': 'rest',
Range: 'bytes=13-240', Range: 'bytes=13-240',
}, },
}).then(response => { }).catch(e => e);
expect(response.status).toBe(404); expect(response.status).toBe(404);
const body = response.text; const body = response.text;
expect(body).toEqual('File not found.'); expect(body).toEqual('File not found.');
done();
});
}); });
}); });

View File

@@ -228,22 +228,35 @@ export class GridFSBucketAdapter extends FilesAdapter {
const partialstart = parts[0]; const partialstart = parts[0];
const partialend = parts[1]; const partialend = parts[1];
const start = parseInt(partialstart, 10); const fileLength = files[0].length;
const end = partialend ? parseInt(partialend, 10) : files[0].length - 1; const fileStart = parseInt(partialstart, 10);
const fileEnd = partialend ? parseInt(partialend, 10) : fileLength;
res.writeHead(206, { let start = Math.min(fileStart || 0, fileEnd, fileLength);
'Accept-Ranges': 'bytes', let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength;
'Content-Length': end - start + 1, if (isNaN(fileStart)) {
'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length, start = fileLength - end + 1;
'Content-Type': contentType, end = fileLength;
}); }
end = Math.min(end, fileLength);
start = Math.max(start, 0);
res.status(206);
res.header('Accept-Ranges', 'bytes');
res.header('Content-Length', end - start);
res.header('Content-Range', 'bytes ' + start + '-' + end + '/' + fileLength);
res.header('Content-Type', contentType);
const stream = bucket.openDownloadStreamByName(filename); const stream = bucket.openDownloadStreamByName(filename);
stream.start(start); stream.start(start);
if (end) {
stream.end(end);
}
stream.on('data', chunk => { stream.on('data', chunk => {
res.write(chunk); res.write(chunk);
}); });
stream.on('error', () => { stream.on('error', (e) => {
res.sendStatus(404); res.status(404);
res.send(e.message);
}); });
stream.on('end', () => { stream.on('end', () => {
res.end(); res.end();

View File

@@ -266,5 +266,10 @@ export class FilesRouter {
} }
function isFileStreamable(req, filesController) { function isFileStreamable(req, filesController) {
return req.get('Range') && typeof filesController.adapter.handleFileStream === 'function'; const range = (req.get('Range') || '/-/').split('-');
const start = Number(range[0]);
const end = Number(range[1]);
return (
(!isNaN(start) || !isNaN(end)) && typeof filesController.adapter.handleFileStream === 'function'
);
} }