Parse Server crashes when receiving file download request with invalid byte range
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Versions prior to 4.10.17, and prior to 5.2.8 on the 5.x branch, crash when a file download request is received with an invalid byte range, resulting in a Denial of Service. This issue has been patched in versions 4.10.17, and 5.2.8. There are no known workarounds.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | < 4.10.17 | 4.10.17 |
parse-servernpm | >= 5.0.0, < 5.2.8 | 5.2.8 |
Affected products
1- Range: < 4.10.17
Patches
23d7a61ecd523fix: 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)) (#8236)
3 files changed · +228 −21
spec/ParseFile.spec.js+199 −10 modified@@ -661,7 +661,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', + }, + }).catch(e => e); + 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 => { const headers = { 'Content-Type': 'application/octet-stream', @@ -750,7 +941,7 @@ describe('Parse.File testing', () => { }); }); - xit('supports getting last n bytes', done => { + it('supports getting last n bytes', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -848,21 +1039,19 @@ describe('Parse.File testing', () => { }); }); - it('fails to stream unknown file', done => { - request({ + it('fails to stream unknown file', async () => { + const response = await request({ url: 'http://localhost:8378/1/files/test/file.txt', headers: { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then(response => { - expect(response.status).toBe(404); - const body = response.text; - expect(body).toEqual('File not found.'); - done(); - }); + }).catch(e => e); + expect(response.status).toBe(404); + const body = response.text; + expect(body).toEqual('File not found.'); }); });
src/Adapters/Files/GridFSBucketAdapter.js+23 −10 modified@@ -228,22 +228,35 @@ export class GridFSBucketAdapter extends FilesAdapter { const partialstart = parts[0]; const partialend = parts[1]; - const start = parseInt(partialstart, 10); - const end = partialend ? parseInt(partialend, 10) : files[0].length - 1; + const fileLength = files[0].length; + const fileStart = parseInt(partialstart, 10); + const fileEnd = partialend ? parseInt(partialend, 10) : fileLength; - res.writeHead(206, { - 'Accept-Ranges': 'bytes', - 'Content-Length': end - start + 1, - 'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length, - 'Content-Type': contentType, - }); + let start = Math.min(fileStart || 0, fileEnd, fileLength); + let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength; + if (isNaN(fileStart)) { + start = fileLength - end + 1; + 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); stream.start(start); + if (end) { + stream.end(end); + } stream.on('data', chunk => { res.write(chunk); }); - stream.on('error', () => { - res.sendStatus(404); + stream.on('error', (e) => { + res.status(404); + res.send(e.message); }); stream.on('end', () => { res.end();
src/Routers/FilesRouter.js+6 −1 modified@@ -243,5 +243,10 @@ export class FilesRouter { } 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' + ); }
066f29673ab4fix: 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)) (#8235)
3 files changed · +228 −21
spec/ParseFile.spec.js+199 −10 modified@@ -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', + }, + }).catch(e => e); + 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 => { const headers = { '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 = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', @@ -879,21 +1070,19 @@ describe('Parse.File testing', () => { }); }); - it('fails to stream unknown file', done => { - request({ + it('fails to stream unknown file', async () => { + const response = await request({ url: 'http://localhost:8378/1/files/test/file.txt', headers: { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', Range: 'bytes=13-240', }, - }).then(response => { - expect(response.status).toBe(404); - const body = response.text; - expect(body).toEqual('File not found.'); - done(); - }); + }).catch(e => e); + expect(response.status).toBe(404); + const body = response.text; + expect(body).toEqual('File not found.'); }); });
src/Adapters/Files/GridFSBucketAdapter.js+23 −10 modified@@ -228,22 +228,35 @@ export class GridFSBucketAdapter extends FilesAdapter { const partialstart = parts[0]; const partialend = parts[1]; - const start = parseInt(partialstart, 10); - const end = partialend ? parseInt(partialend, 10) : files[0].length - 1; + const fileLength = files[0].length; + const fileStart = parseInt(partialstart, 10); + const fileEnd = partialend ? parseInt(partialend, 10) : fileLength; - res.writeHead(206, { - 'Accept-Ranges': 'bytes', - 'Content-Length': end - start + 1, - 'Content-Range': 'bytes ' + start + '-' + end + '/' + files[0].length, - 'Content-Type': contentType, - }); + let start = Math.min(fileStart || 0, fileEnd, fileLength); + let end = Math.max(fileStart || 0, fileEnd) + 1 || fileLength; + if (isNaN(fileStart)) { + start = fileLength - end + 1; + 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); stream.start(start); + if (end) { + stream.end(end); + } stream.on('data', chunk => { res.write(chunk); }); - stream.on('error', () => { - res.sendStatus(404); + stream.on('error', (e) => { + res.status(404); + res.send(e.message); }); stream.on('end', () => { res.end();
src/Routers/FilesRouter.js+6 −1 modified@@ -271,5 +271,10 @@ export class FilesRouter { } 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' + ); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-h423-w6qv-2wj3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-39313ghsaADVISORY
- github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612aghsaWEB
- github.com/parse-community/parse-server/commit/3d7a61ecd5231638f01ff1a965b6313043c594a7ghsaWEB
- github.com/parse-community/parse-server/releases/tag/4.10.17ghsaWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-h423-w6qv-2wj3ghsaWEB
News mentions
0No linked articles in our index yet.