From ba2b0a9cb9a568817a114b132a4c2e0911d76df1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Jun 2022 18:29:26 +0200 Subject: [PATCH 01/14] fix: certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) --- release.config.js | 11 +-- spec/AuthenticationAdapters.spec.js | 147 +++++++++++++++++++++++++--- spec/support/cert/game_center.pem | 28 ++++++ src/Adapters/Auth/gcenter.js | 101 +++++++++++++++---- 4 files changed, 249 insertions(+), 38 deletions(-) create mode 100644 spec/support/cert/game_center.pem diff --git a/release.config.js b/release.config.js index af2bd2c9f3..c31eb3ecb8 100644 --- a/release.config.js +++ b/release.config.js @@ -83,21 +83,20 @@ async function config() { ['@semantic-release/git', { assets: [changelogFile, 'package.json', 'package-lock.json', 'npm-shrinkwrap.json'], }], + ['@semantic-release/github', { + successComment: getReleaseComment(), + labels: ['type:ci'], + releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>'] + }], [ "@saithodev/semantic-release-backmerge", { "branches": [ { from: "beta", to: "alpha" }, { from: "release", to: "beta" }, - { from: "release", to: "alpha" }, ] } ], - ['@semantic-release/github', { - successComment: getReleaseComment(), - labels: ['type:ci'], - releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>'] - }], ], }; diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index ca2d35363d..5c0123f3b7 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1652,8 +1652,41 @@ describe('apple signin auth adapter', () => { describe('Apple Game Center Auth adapter', () => { const gcenter = require('../lib/Adapters/Auth/gcenter'); - + const fs = require('fs'); + const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem'); + it('can load adapter', async () => { + const options = { + gcenter: { + rootCertificateUrl: + 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem', + }, + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + options + ); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); + }); it('validateAuthData should validate', async () => { + const options = { + gcenter: { + rootCertificateUrl: + 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem', + }, + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + options + ); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); // real token is used const authData = { id: 'G:1965586982', @@ -1664,29 +1697,49 @@ describe('Apple Game Center Auth adapter', () => { salt: 'DzqqrQ==', bundleId: 'cloud.xtralife.gamecenterauth', }; - + gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert; await gcenter.validateAuthData(authData); }); it('validateAuthData invalid signature id', async () => { + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + {} + ); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); const authData = { id: 'G:1965586982', - publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer', + publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer', timestamp: 1565257031287, signature: '1234', salt: 'DzqqrQ==', - bundleId: 'cloud.xtralife.gamecenterauth', + bundleId: 'com.example.com', }; - - try { - await gcenter.validateAuthData(authData); - fail(); - } catch (e) { - expect(e.message).toBe('Apple Game Center - invalid signature'); - } + await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature') + ); }); it('validateAuthData invalid public key http url', async () => { + const options = { + gcenter: { + rootCertificateUrl: + 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem', + }, + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + options + ); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); const publicKeyUrls = [ 'example.com', 'http://static.gc.apple.com/public-key/gc-prod-4.cer', @@ -1714,6 +1767,78 @@ describe('Apple Game Center Auth adapter', () => { ) ); }); + + it('should not validate Symantec Cert', async () => { + const options = { + gcenter: { + rootCertificateUrl: + 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem', + }, + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + options + ); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); + expect(() => + gcenter.verifyPublicKeyIssuer( + testCert, + 'https://static.gc.apple.com/public-key/gc-prod-4.cer' + ) + ); + }); + + it('adapter should load default cert', async () => { + const options = { + gcenter: {}, + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + options + ); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); + const previous = new Date(); + await adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ); + + const duration = new Date().getTime() - previous.getTime(); + expect(duration).toEqual(0); + }); + + it('adapter should throw', async () => { + const options = { + gcenter: { + rootCertificateUrl: 'https://example.com', + }, + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'gcenter', + options + ); + await expectAsync( + adapter.validateAppId( + appIds, + { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' }, + providerOptions + ) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.' + ) + ); + }); }); describe('phant auth adapter', () => { diff --git a/spec/support/cert/game_center.pem b/spec/support/cert/game_center.pem new file mode 100644 index 0000000000..b5dffcd832 --- /dev/null +++ b/spec/support/cert/game_center.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEvDCCA6SgAwIBAgIQXRHxNXkw1L9z5/3EZ/T/hDANBgkqhkiG9w0BAQsFADB/ +MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAd +BgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMDAuBgNVBAMTJ1N5bWFudGVj +IENsYXNzIDMgU0hBMjU2IENvZGUgU2lnbmluZyBDQTAeFw0xODA5MTcwMDAwMDBa +Fw0xOTA5MTcyMzU5NTlaMHMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y +bmlhMRIwEAYDVQQHDAlDdXBlcnRpbm8xFDASBgNVBAoMC0FwcGxlLCBJbmMuMQ8w +DQYDVQQLDAZHQyBTUkUxFDASBgNVBAMMC0FwcGxlLCBJbmMuMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA06fwIi8fgKrTQu7cBcFkJVF6+Tqvkg7MKJTM +IOYPPQtPF3AZYPsbUoRKAD7/JXrxxOSVJ7vU1mP77tYG8TcUteZ3sAwvt2dkRbm7 +ZO6DcmSggv1Dg4k3goNw4GYyCY4Z2/8JSmsQ80Iv/UOOwynpBziEeZmJ4uck6zlA +17cDkH48LBpKylaqthym5bFs9gj11pto7mvyb5BTcVuohwi6qosvbs/4VGbC2Nsz +ie416nUZfv+xxoXH995gxR2mw5cDdeCew7pSKxEhvYjT2nVdQF0q/hnPMFnOaEyT +q79n3gwFXyt0dy8eP6KBF7EW9J6b7ubu/j7h+tQfxPM+gTXOBQIDAQABo4IBPjCC +ATowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH +AwMwYQYDVR0gBFowWDBWBgZngQwBBAEwTDAjBggrBgEFBQcCARYXaHR0cHM6Ly9k +LnN5bWNiLmNvbS9jcHMwJQYIKwYBBQUHAgIwGQwXaHR0cHM6Ly9kLnN5bWNiLmNv +bS9ycGEwHwYDVR0jBBgwFoAUljtT8Hkzl699g+8uK8zKt4YecmYwKwYDVR0fBCQw +IjAgoB6gHIYaaHR0cDovL3N2LnN5bWNiLmNvbS9zdi5jcmwwVwYIKwYBBQUHAQEE +SzBJMB8GCCsGAQUFBzABhhNodHRwOi8vc3Yuc3ltY2QuY29tMCYGCCsGAQUFBzAC +hhpodHRwOi8vc3Yuc3ltY2IuY29tL3N2LmNydDANBgkqhkiG9w0BAQsFAAOCAQEA +I/j/PcCNPebSAGrcqSFBSa2mmbusOX01eVBg8X0G/z8Z+ZWUfGFzDG0GQf89MPxV +woec+nZuqui7o9Bg8s8JbHV0TC52X14CbTj9w/qBF748WbH9gAaTkrJYPm+MlNhu +tjEuQdNl/YXVMvQW4O8UMHTi09GyJQ0NC4q92Wxvx1m/qzjvTLvrXHGQ9pEHhPyz +vfBLxQkWpNoCNKU7UeESyH06XOrGc9MsII9deeKsDJp9a0jtx+pP4MFVtFME9SSQ +tMBs0It7WwEf7qcRLpialxKwY2EzQ9g4WnANHqo18PrDBE10TFpZPzUh7JhMViVr +EEbl0YdElmF8Hlamah/yNw== +-----END CERTIFICATE----- diff --git a/src/Adapters/Auth/gcenter.js b/src/Adapters/Auth/gcenter.js index 5cd8e8affc..f70c254188 100644 --- a/src/Adapters/Auth/gcenter.js +++ b/src/Adapters/Auth/gcenter.js @@ -14,7 +14,8 @@ const authData = { const { Parse } = require('parse/node'); const crypto = require('crypto'); const https = require('https'); - +const { pki } = require('node-forge'); +const ca = { cert: null, url: null }; const cache = {}; // (publicKey -> cert) cache function verifyPublicKeyUrl(publicKeyUrl) { @@ -52,39 +53,53 @@ async function getAppleCertificate(publicKeyUrl) { path: url.pathname, method: 'HEAD', }; - const headers = await new Promise((resolve, reject) => + const cert_headers = await new Promise((resolve, reject) => https.get(headOptions, res => resolve(res.headers)).on('error', reject) ); + const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert']; if ( - headers['content-type'] !== 'application/pkix-cert' || - headers['content-length'] == null || - headers['content-length'] > 10000 + !validContentTypes.includes(cert_headers['content-type']) || + cert_headers['content-length'] == null || + cert_headers['content-length'] > 10000 ) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}` ); } + const { certificate, headers } = await getCertificate(publicKeyUrl); + if (headers['cache-control']) { + const expire = headers['cache-control'].match(/max-age=([0-9]+)/); + if (expire) { + cache[publicKeyUrl] = certificate; + // we'll expire the cache entry later, as per max-age + setTimeout(() => { + delete cache[publicKeyUrl]; + }, parseInt(expire[1], 10) * 1000); + } + } + return verifyPublicKeyIssuer(certificate, publicKeyUrl); +} + +function getCertificate(url, buffer) { return new Promise((resolve, reject) => { https - .get(publicKeyUrl, res => { - let data = ''; + .get(url, res => { + const data = []; res.on('data', chunk => { - data += chunk.toString('base64'); + data.push(chunk); }); res.on('end', () => { - const cert = convertX509CertToPEM(data); - if (res.headers['cache-control']) { - var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/); - if (expire) { - cache[publicKeyUrl] = cert; - // we'll expire the cache entry later, as per max-age - setTimeout(() => { - delete cache[publicKeyUrl]; - }, parseInt(expire[1], 10) * 1000); - } + if (buffer) { + resolve({ certificate: Buffer.concat(data), headers: res.headers }); + return; } - resolve(cert); + let cert = ''; + for (const chunk of data) { + cert += chunk.toString('base64'); + } + const certificate = convertX509CertToPEM(cert); + resolve({ certificate, headers: res.headers }); }); }) .on('error', reject); @@ -115,6 +130,30 @@ function verifySignature(publicKey, authData) { } } +function verifyPublicKeyIssuer(cert, publicKeyUrl) { + const publicKeyCert = pki.certificateFromPem(cert); + if (!ca.cert) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.' + ); + } + try { + if (!ca.cert.verify(publicKeyCert)) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}` + ); + } + } catch (e) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + `Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}` + ); + } + return cert; +} + // Returns a promise that fulfills if this user id is valid. async function validateAuthData(authData) { if (!authData.id) { @@ -126,11 +165,31 @@ async function validateAuthData(authData) { } // Returns a promise that fulfills if this app id is valid. -function validateAppId() { - return Promise.resolve(); +async function validateAppId(appIds, authData, options = {}) { + if (!options.rootCertificateUrl) { + options.rootCertificateUrl = + 'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem'; + } + if (ca.url === options.rootCertificateUrl) { + return; + } + const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true); + if ( + headers['content-type'] !== 'application/x-pem-file' || + headers['content-length'] == null || + headers['content-length'] > 10000 + ) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.' + ); + } + ca.cert = pki.certificateFromPem(certificate); + ca.url = options.rootCertificateUrl; } module.exports = { validateAppId, validateAuthData, + cache, }; From ed0baa87af0769086bdaeb5fba0f9ce6279838a0 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 17 Jun 2022 16:36:47 +0000 Subject: [PATCH 02/14] chore(release): 5.2.2 [skip ci] ## [5.2.2](https://github.com/parse-community/parse-server/compare/5.2.1...5.2.2) (2022-06-17) ### Bug Fixes * certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) ([ba2b0a9](https://github.com/parse-community/parse-server/commit/ba2b0a9cb9a568817a114b132a4c2e0911d76df1)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 7e4c801fb7..204f33a586 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.2](https://github.com/parse-community/parse-server/compare/5.2.1...5.2.2) (2022-06-17) + + +### Bug Fixes + +* certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) ([ba2b0a9](https://github.com/parse-community/parse-server/commit/ba2b0a9cb9a568817a114b132a4c2e0911d76df1)) + ## [5.2.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1) (2022-05-01) diff --git a/package-lock.json b/package-lock.json index 542f444268..3c12d6cc48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.1", + "version": "5.2.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 708df9717d..6de85ea502 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.1", + "version": "5.2.2", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 5be375dec2fa35425c1003ae81c55995ac72af92 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 18 Jun 2022 01:33:19 +0200 Subject: [PATCH 03/14] fix: invalid file request not properly handled; this fixes a security vulnerability in which an invalid file request can crash the server ([GHSA-xw6g-jjvf-wwf9](https://github.com/parse-community/parse-server/security/advisories/GHSA-xw6g-jjvf-wwf9)) (#8060) --- spec/ParseFile.spec.js | 38 ++++++++++++++++++++++++++++++++++++++ src/Routers/FilesRouter.js | 12 +++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index af1de35ee7..281ce45cc2 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -654,6 +654,44 @@ describe('Parse.File testing', () => { }); }); + describe('getting files', () => { + it('does not crash on file request with invalid app ID', async () => { + const res1 = await request({ + url: 'http://localhost:8378/1/files/invalid-id/invalid-file.txt', + }).catch(e => e); + expect(res1.status).toBe(403); + expect(res1.data).toEqual({ code: 119, error: 'Invalid application ID.' }); + // Ensure server did not crash + const res2 = await request({ url: 'http://localhost:8378/1/health' }); + expect(res2.status).toEqual(200); + expect(res2.data).toEqual({ status: 'ok' }); + }); + + it('does not crash on file request with invalid path', async () => { + const res1 = await request({ + url: 'http://localhost:8378/1/files/invalid-id//invalid-path/%20/invalid-file.txt', + }).catch(e => e); + expect(res1.status).toBe(403); + expect(res1.data).toEqual({ error: 'unauthorized' }); + // Ensure server did not crash + const res2 = await request({ url: 'http://localhost:8378/1/health' }); + expect(res2.status).toEqual(200); + expect(res2.data).toEqual({ status: 'ok' }); + }); + + it('does not crash on file metadata request with invalid app ID', async () => { + const res1 = await request({ + url: `http://localhost:8378/1/files/invalid-id/metadata/invalid-file.txt`, + }); + expect(res1.status).toBe(200); + expect(res1.data).toEqual({}); + // Ensure server did not crash + const res2 = await request({ url: 'http://localhost:8378/1/health' }); + expect(res2.status).toEqual(200); + expect(res2.data).toEqual({ status: 'ok' }); + }); + }); + xdescribe('Gridstore Range tests', () => { it('supports range requests', done => { const headers = { diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index c0c7e00f13..bdd86ae59b 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -66,6 +66,12 @@ export class FilesRouter { getHandler(req, res) { const config = Config.get(req.params.appId); + if (!config) { + res.status(403); + const err = new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Invalid application ID.'); + res.json({ code: err.code, error: err.message }); + return; + } const filesController = config.filesController; const filename = req.params.filename; const contentType = mime.getType(filename); @@ -250,10 +256,10 @@ export class FilesRouter { } async metadataHandler(req, res) { - const config = Config.get(req.params.appId); - const { filesController } = config; - const { filename } = req.params; try { + const config = Config.get(req.params.appId); + const { filesController } = config; + const { filename } = req.params; const data = await filesController.getMetadata(filename); res.status(200); res.json(data); From eb2952fff7e73308a20672d2ee2cdc4130d4f26c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 17 Jun 2022 23:40:39 +0000 Subject: [PATCH 04/14] chore(release): 5.2.3 [skip ci] ## [5.2.3](https://github.com/parse-community/parse-server/compare/5.2.2...5.2.3) (2022-06-17) ### Bug Fixes * invalid file request not properly handled; this fixes a security vulnerability in which an invalid file request can crash the server ([GHSA-xw6g-jjvf-wwf9](https://github.com/parse-community/parse-server/security/advisories/GHSA-xw6g-jjvf-wwf9)) ([#8060](https://github.com/parse-community/parse-server/issues/8060)) ([5be375d](https://github.com/parse-community/parse-server/commit/5be375dec2fa35425c1003ae81c55995ac72af92)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 204f33a586..e6b075405a 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.3](https://github.com/parse-community/parse-server/compare/5.2.2...5.2.3) (2022-06-17) + + +### Bug Fixes + +* invalid file request not properly handled; this fixes a security vulnerability in which an invalid file request can crash the server ([GHSA-xw6g-jjvf-wwf9](https://github.com/parse-community/parse-server/security/advisories/GHSA-xw6g-jjvf-wwf9)) ([#8060](https://github.com/parse-community/parse-server/issues/8060)) ([5be375d](https://github.com/parse-community/parse-server/commit/5be375dec2fa35425c1003ae81c55995ac72af92)) + ## [5.2.2](https://github.com/parse-community/parse-server/compare/5.2.1...5.2.2) (2022-06-17) diff --git a/package-lock.json b/package-lock.json index 3c12d6cc48..c8c7d6d632 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.2", + "version": "5.2.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6de85ea502..f1aeb2774c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.2", + "version": "5.2.3", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 309f64ced8700321df056fb3cc97f15007a00df1 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Thu, 30 Jun 2022 12:26:39 +0200 Subject: [PATCH 05/14] fix: protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) (#8073) --- spec/ParseLiveQuery.spec.js | 46 ++++++++++++ src/Controllers/DatabaseController.js | 14 +++- src/LiveQuery/ParseCloudCodePublisher.js | 3 + src/LiveQuery/ParseLiveQueryServer.js | 93 +++++++++++++++++++----- 4 files changed, 132 insertions(+), 24 deletions(-) diff --git a/spec/ParseLiveQuery.spec.js b/spec/ParseLiveQuery.spec.js index d9b79bc588..b439b81ef2 100644 --- a/spec/ParseLiveQuery.spec.js +++ b/spec/ParseLiveQuery.spec.js @@ -1066,6 +1066,52 @@ describe('ParseLiveQuery', function () { } }); + it('should strip out protected fields', async () => { + await reconfigureServer({ + liveQuery: { classNames: ['Test'] }, + startLiveQueryServer: true, + }); + const obj1 = new Parse.Object('Test'); + obj1.set('foo', 'foo'); + obj1.set('bar', 'bar'); + obj1.set('qux', 'qux'); + await obj1.save(); + const config = Config.get(Parse.applicationId); + const schemaController = await config.database.loadSchema(); + await schemaController.updateClass( + 'Test', + {}, + { + get: { '*': true }, + find: { '*': true }, + update: { '*': true }, + protectedFields: { + '*': ['foo'], + }, + } + ); + const object = await obj1.fetch(); + expect(object.get('foo')).toBe(undefined); + expect(object.get('bar')).toBeDefined(); + expect(object.get('qux')).toBeDefined(); + + const subscription = await new Parse.Query('Test').subscribe(); + await Promise.all([ + new Promise(resolve => { + subscription.on('update', (obj, original) => { + expect(obj.get('foo')).toBe(undefined); + expect(obj.get('bar')).toBeDefined(); + expect(obj.get('qux')).toBeDefined(); + expect(original.get('foo')).toBe(undefined); + expect(original.get('bar')).toBeDefined(); + expect(original.get('qux')).toBeDefined(); + resolve(); + }); + }), + obj1.save({ foo: 'abc' }), + ]); + }); + afterEach(async function (done) { const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); client.close(); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 3e69b1f5eb..8e99b29216 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -127,7 +127,7 @@ const filterSensitiveData = ( aclGroup: any[], auth: any, operation: any, - schema: SchemaController.SchemaController, + schema: SchemaController.SchemaController | any, className: string, protectedFields: null | Array, object: any @@ -136,7 +136,8 @@ const filterSensitiveData = ( if (auth && auth.user) userId = auth.user.id; // replace protectedFields when using pointer-permissions - const perms = schema.getClassLevelPermissions(className); + const perms = + schema && schema.getClassLevelPermissions ? schema.getClassLevelPermissions(className) : {}; if (perms) { const isReadOperation = ['get', 'find'].indexOf(operation) > -1; @@ -1533,14 +1534,17 @@ class DatabaseController { } addProtectedFields( - schema: SchemaController.SchemaController, + schema: SchemaController.SchemaController | any, className: string, query: any = {}, aclGroup: any[] = [], auth: any = {}, queryOptions: FullQueryOptions = {} ): null | string[] { - const perms = schema.getClassLevelPermissions(className); + const perms = + schema && schema.getClassLevelPermissions + ? schema.getClassLevelPermissions(className) + : schema; if (!perms) return null; const protectedFields = perms.protectedFields; @@ -1806,8 +1810,10 @@ class DatabaseController { } static _validateQuery: any => void; + static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void; } module.exports = DatabaseController; // Expose validateQuery for tests module.exports._validateQuery = validateQuery; +module.exports.filterSensitiveData = filterSensitiveData; diff --git a/src/LiveQuery/ParseCloudCodePublisher.js b/src/LiveQuery/ParseCloudCodePublisher.js index 85e95121fb..7d306d1b57 100644 --- a/src/LiveQuery/ParseCloudCodePublisher.js +++ b/src/LiveQuery/ParseCloudCodePublisher.js @@ -33,6 +33,9 @@ class ParseCloudCodePublisher { if (request.original) { message.originalParseObject = request.original._toFullJSON(); } + if (request.classLevelPermissions) { + message.classLevelPermissions = request.classLevelPermissions; + } this.parsePublisher.publish(type, JSON.stringify(message)); } } diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index fa05f23711..f2a27a8080 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub'; import SchemaController from '../Controllers/SchemaController'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers'; +import { + runLiveQueryEventHandlers, + getTrigger, + runTrigger, + resolveError, + toJSONwithObjects, +} from '../triggers'; import { getAuthForSessionToken, Auth } from '../Auth'; -import { getCacheController } from '../Controllers'; +import { getCacheController, getDatabaseController } from '../Controllers'; import LRU from 'lru-cache'; import UserRouter from '../Routers/UsersRouter'; +import DatabaseController from '../Controllers/DatabaseController'; class ParseLiveQueryServer { clients: Map; @@ -185,14 +192,14 @@ class ParseLiveQueryServer { if (res.object && typeof res.object.toJSON === 'function') { deletedParseObject = toJSONwithObjects(res.object, res.object.className || className); } - if ( - (deletedParseObject.className === '_User' || - deletedParseObject.className === '_Session') && - !client.hasMasterKey - ) { - delete deletedParseObject.sessionToken; - delete deletedParseObject.authData; - } + await this._filterSensitiveData( + classLevelPermissions, + res, + client, + requestId, + op, + subscription.query + ); client.pushDelete(requestId, deletedParseObject); } catch (e) { const error = resolveError(e); @@ -339,16 +346,14 @@ class ParseLiveQueryServer { res.original.className || className ); } - if ( - (currentParseObject.className === '_User' || - currentParseObject.className === '_Session') && - !client.hasMasterKey - ) { - delete currentParseObject.sessionToken; - delete originalParseObject?.sessionToken; - delete currentParseObject.authData; - delete originalParseObject?.authData; - } + await this._filterSensitiveData( + classLevelPermissions, + res, + client, + requestId, + op, + subscription.query + ); const functionName = 'push' + res.event.charAt(0).toUpperCase() + res.event.slice(1); if (client[functionName]) { client[functionName](requestId, currentParseObject, originalParseObject); @@ -540,6 +545,54 @@ class ParseLiveQueryServer { // return rolesQuery.find({useMasterKey:true}); } + async _filterSensitiveData( + classLevelPermissions: ?any, + res: any, + client: any, + requestId: number, + op: string, + query: any + ) { + const subscriptionInfo = client.getSubscriptionInfo(requestId); + const aclGroup = ['*']; + let clientAuth; + if (typeof subscriptionInfo !== 'undefined') { + const { userId, auth } = await this.getAuthForSessionToken(subscriptionInfo.sessionToken); + if (userId) { + aclGroup.push(userId); + } + clientAuth = auth; + } + const filter = obj => { + if (!obj) { + return; + } + let protectedFields = classLevelPermissions?.protectedFields || []; + if (!client.hasMasterKey && !Array.isArray(protectedFields)) { + protectedFields = getDatabaseController(this.config).addProtectedFields( + classLevelPermissions, + res.object.className, + query, + aclGroup, + clientAuth + ); + } + return DatabaseController.filterSensitiveData( + client.hasMasterKey, + aclGroup, + clientAuth, + op, + classLevelPermissions, + res.object.className, + protectedFields, + obj, + query + ); + }; + res.object = filter(res.object); + res.original = filter(res.original); + } + _getCLPOperation(query: any) { return typeof query === 'object' && Object.keys(query).length == 1 && From e42be5c526fc1389fbf3964c24a6b6a29b075522 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 30 Jun 2022 10:46:12 +0000 Subject: [PATCH 06/14] chore(release): 5.2.4 [skip ci] ## [5.2.4](https://github.com/parse-community/parse-server/compare/5.2.3...5.2.4) (2022-06-30) ### Bug Fixes * protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) ([#8073](https://github.com/parse-community/parse-server/issues/8073)) ([309f64c](https://github.com/parse-community/parse-server/commit/309f64ced8700321df056fb3cc97f15007a00df1)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index e6b075405a..0c8eb7acae 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.4](https://github.com/parse-community/parse-server/compare/5.2.3...5.2.4) (2022-06-30) + + +### Bug Fixes + +* protected fields exposed via LiveQuery; this removes protected fields from the client response; this may be a breaking change if your app is currently expecting to receive these protected fields ([GHSA-crrq-vr9j-fxxh](https://github.com/parse-community/parse-server/security/advisories/GHSA-crrq-vr9j-fxxh)) (https://github.com/parse-community/parse-server/pull/8074) ([#8073](https://github.com/parse-community/parse-server/issues/8073)) ([309f64c](https://github.com/parse-community/parse-server/commit/309f64ced8700321df056fb3cc97f15007a00df1)) + ## [5.2.3](https://github.com/parse-community/parse-server/compare/5.2.2...5.2.3) (2022-06-17) diff --git a/package-lock.json b/package-lock.json index c8c7d6d632..0fe779a440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.3", + "version": "5.2.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f1aeb2774c..a7bcb2e34a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.3", + "version": "5.2.4", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From e39d51bd329cd978589983bd659db46e1d45aad4 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 2 Sep 2022 21:13:18 +0200 Subject: [PATCH 07/14] fix: brute force guessing of user sensitive data via search patterns; this fixes a security vulnerability in which internal and protected fields may be used as query constraints to guess the value of these fields and obtain sensitive data (GHSA-2m6g-crv8-p3c6) (#8144) --- spec/RestQuery.spec.js | 73 +++++++++++++++++++++++++++ src/Controllers/DatabaseController.js | 71 +++++++++++++------------- src/RestQuery.js | 27 ++++++++++ 3 files changed, 134 insertions(+), 37 deletions(-) diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index c8e3adb49d..85d9f87fec 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -191,6 +191,79 @@ describe('rest query', () => { expect(result.results.length).toEqual(0); }); + it('query internal field', async () => { + const internalFields = [ + '_email_verify_token', + '_perishable_token', + '_tombstone', + '_email_verify_token_expires_at', + '_failed_login_count', + '_account_lockout_expires_at', + '_password_changed_at', + '_password_history', + ]; + await Promise.all([ + ...internalFields.map(field => + expectAsync(new Parse.Query(Parse.User).exists(field).find()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${field}`) + ) + ), + ...internalFields.map(field => + new Parse.Query(Parse.User).exists(field).find({ useMasterKey: true }) + ), + ]); + }); + + it('query protected field', async () => { + const user = new Parse.User(); + user.setUsername('username1'); + user.setPassword('password'); + await user.signUp(); + const config = Config.get(Parse.applicationId); + const obj = new Parse.Object('Test'); + + obj.set('owner', user); + obj.set('test', 'test'); + obj.set('zip', 1234); + await obj.save(); + + const schema = await config.database.loadSchema(); + await schema.updateClass( + 'Test', + {}, + { + get: { '*': true }, + find: { '*': true }, + protectedFields: { [user.id]: ['zip'] }, + } + ); + await Promise.all([ + new Parse.Query('Test').exists('test').find(), + expectAsync(new Parse.Query('Test').exists('zip').find()).toBeRejectedWith( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'This user is not allowed to query zip on class Test' + ) + ), + ]); + }); + + it('query protected field with matchesQuery', async () => { + const user = new Parse.User(); + user.setUsername('username1'); + user.setPassword('password'); + await user.signUp(); + const test = new Parse.Object('TestObject', { user }); + await test.save(); + const subQuery = new Parse.Query(Parse.User); + subQuery.exists('_perishable_token'); + await expectAsync( + new Parse.Query('TestObject').matchesQuery('user', subQuery).find() + ).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid key name: _perishable_token') + ); + }); + it('query with wrongly encoded parameter', done => { rest .create(config, nobody, 'TestParameterEncode', { foo: 'bar' }) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 8e99b29216..25b97d0ec7 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -55,31 +55,27 @@ const transformObjectACL = ({ ACL, ...result }) => { return result; }; -const specialQuerykeys = [ - '$and', - '$or', - '$nor', - '_rperm', - '_wperm', - '_perishable_token', +const specialQueryKeys = ['$and', '$or', '$nor', '_rperm', '_wperm']; +const specialMasterQueryKeys = [ + ...specialQueryKeys, '_email_verify_token', + '_perishable_token', + '_tombstone', '_email_verify_token_expires_at', - '_account_lockout_expires_at', '_failed_login_count', + '_account_lockout_expires_at', + '_password_changed_at', + '_password_history', ]; -const isSpecialQueryKey = key => { - return specialQuerykeys.indexOf(key) >= 0; -}; - -const validateQuery = (query: any): void => { +const validateQuery = (query: any, isMaster: boolean, update: boolean): void => { if (query.ACL) { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); } if (query.$or) { if (query.$or instanceof Array) { - query.$or.forEach(validateQuery); + query.$or.forEach(value => validateQuery(value, isMaster, update)); } else { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); } @@ -87,7 +83,7 @@ const validateQuery = (query: any): void => { if (query.$and) { if (query.$and instanceof Array) { - query.$and.forEach(validateQuery); + query.$and.forEach(value => validateQuery(value, isMaster, update)); } else { throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); } @@ -95,7 +91,7 @@ const validateQuery = (query: any): void => { if (query.$nor) { if (query.$nor instanceof Array && query.$nor.length > 0) { - query.$nor.forEach(validateQuery); + query.$nor.forEach(value => validateQuery(value, isMaster, update)); } else { throw new Parse.Error( Parse.Error.INVALID_QUERY, @@ -115,7 +111,11 @@ const validateQuery = (query: any): void => { } } } - if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + if ( + !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/) && + ((!specialQueryKeys.includes(key) && !isMaster && !update) || + (update && isMaster && !specialMasterQueryKeys.includes(key))) + ) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); } }); @@ -208,27 +208,24 @@ const filterSensitiveData = ( perms.protectedFields.temporaryKeys.forEach(k => delete object[k]); } - if (!isUserClass) { - return object; + if (isUserClass) { + object.password = object._hashed_password; + delete object._hashed_password; + delete object.sessionToken; } - object.password = object._hashed_password; - delete object._hashed_password; - - delete object.sessionToken; - if (isMaster) { return object; } - delete object._email_verify_token; - delete object._perishable_token; - delete object._perishable_token_expires_at; - delete object._tombstone; - delete object._email_verify_token_expires_at; - delete object._failed_login_count; - delete object._account_lockout_expires_at; - delete object._password_changed_at; - delete object._password_history; + for (const key in object) { + if (key.charAt(0) === '_') { + delete object[key]; + } + } + + if (!isUserClass) { + return object; + } if (aclGroup.indexOf(object.objectId) > -1) { return object; @@ -515,7 +512,7 @@ class DatabaseController { if (acl) { query = addWriteACL(query, acl); } - validateQuery(query); + validateQuery(query, isMaster, true); return schemaController .getOneSchema(className, true) .catch(error => { @@ -761,7 +758,7 @@ class DatabaseController { if (acl) { query = addWriteACL(query, acl); } - validateQuery(query); + validateQuery(query, isMaster, false); return schemaController .getOneSchema(className) .catch(error => { @@ -1253,7 +1250,7 @@ class DatabaseController { query = addReadACL(query, aclGroup); } } - validateQuery(query); + validateQuery(query, isMaster, false); if (count) { if (!classExists) { return 0; @@ -1809,7 +1806,7 @@ class DatabaseController { return Promise.resolve(response); } - static _validateQuery: any => void; + static _validateQuery: (any, boolean, boolean) => void; static filterSensitiveData: (boolean, any[], any, any, any, string, any[], any) => void; } diff --git a/src/RestQuery.js b/src/RestQuery.js index be96683451..a7a4a83f54 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) { .then(() => { return this.buildRestWhere(); }) + .then(() => { + return this.denyProtectedFields(); + }) .then(() => { return this.handleIncludeAll(); }) @@ -688,6 +691,30 @@ RestQuery.prototype.runCount = function () { }); }; +RestQuery.prototype.denyProtectedFields = async function () { + if (this.auth.isMaster) { + return; + } + const schemaController = await this.config.database.loadSchema(); + const protectedFields = + this.config.database.addProtectedFields( + schemaController, + this.className, + this.restWhere, + this.findOptions.acl, + this.auth, + this.findOptions + ) || []; + for (const key of protectedFields) { + if (this.restWhere[key]) { + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + `This user is not allowed to query ${key} on class ${this.className}` + ); + } + } +}; + // Augments this.response with all pointers on an object RestQuery.prototype.handleIncludeAll = function () { if (!this.includeAll) { From 83fd16c1b911de1fee2c6184feff5933de455f56 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 2 Sep 2022 19:20:39 +0000 Subject: [PATCH 08/14] chore(release): 5.2.5 [skip ci] ## [5.2.5](https://github.com/parse-community/parse-server/compare/5.2.4...5.2.5) (2022-09-02) ### Bug Fixes * brute force guessing of user sensitive data via search patterns; this fixes a security vulnerability in which internal and protected fields may be used as query constraints to guess the value of these fields and obtain sensitive data (GHSA-2m6g-crv8-p3c6) ([#8144](https://github.com/parse-community/parse-server/issues/8144)) ([e39d51b](https://github.com/parse-community/parse-server/commit/e39d51bd329cd978589983bd659db46e1d45aad4)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 0c8eb7acae..40eb03a054 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.5](https://github.com/parse-community/parse-server/compare/5.2.4...5.2.5) (2022-09-02) + + +### Bug Fixes + +* brute force guessing of user sensitive data via search patterns; this fixes a security vulnerability in which internal and protected fields may be used as query constraints to guess the value of these fields and obtain sensitive data (GHSA-2m6g-crv8-p3c6) ([#8144](https://github.com/parse-community/parse-server/issues/8144)) ([e39d51b](https://github.com/parse-community/parse-server/commit/e39d51bd329cd978589983bd659db46e1d45aad4)) + ## [5.2.4](https://github.com/parse-community/parse-server/compare/5.2.3...5.2.4) (2022-06-30) diff --git a/package-lock.json b/package-lock.json index 0fe779a440..0ffe4c1645 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.4", + "version": "5.2.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a7bcb2e34a..b574d65d5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.4", + "version": "5.2.5", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 6d0b2f534603301bb630d9c8e497af3bc7ff1d09 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Tue, 20 Sep 2022 02:18:07 +0200 Subject: [PATCH 09/14] fix: session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) (#8182) --- spec/ParseSession.spec.js | 28 ++++++++++++++++++++++++++++ src/RestWrite.js | 14 ++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/spec/ParseSession.spec.js b/spec/ParseSession.spec.js index 084f141e08..176ed152f9 100644 --- a/spec/ParseSession.spec.js +++ b/spec/ParseSession.spec.js @@ -135,4 +135,32 @@ describe('Parse.Session', () => { fail(err); }); }); + + it('cannot edit session with known ID', async () => { + const request = require('../lib/request'); + await setupTestUsers(); + const [first, second] = await new Parse.Query(Parse.Session).find({ useMasterKey: true }); + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'rest', + 'X-Parse-Session-Token': second.get('sessionToken'), + 'Content-Type': 'application/json', + }; + const firstUser = first.get('user').id; + const secondUser = second.get('user').id; + const e = await request({ + method: 'PUT', + headers, + url: `http://localhost:8378/1/sessions/${first.id}`, + body: JSON.stringify({ + foo: 'bar', + user: { __type: 'Pointer', className: '_User', objectId: secondUser }, + }), + }).catch(e => e.data); + expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + expect(e.error).toBe('Object not found.'); + await Parse.Object.fetchAll([first, second], { useMasterKey: true }); + expect(first.get('user').id).toBe(firstUser); + expect(second.get('user').id).toBe(secondUser); + }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 8b728731da..be3eea908c 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1015,6 +1015,20 @@ RestWrite.prototype.handleSession = function () { } else if (this.data.sessionToken) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } + if (!this.auth.isMaster) { + this.query = { + $and: [ + this.query, + { + user: { + __type: 'Pointer', + className: '_User', + objectId: this.auth.user.id, + }, + }, + ], + }; + } } if (!this.query && !this.auth.isMaster) { From 7aac70cca69de8d2e859dd31414163816c813bb1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Sep 2022 00:27:18 +0000 Subject: [PATCH 10/14] chore(release): 5.2.6 [skip ci] ## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (2022-09-20) ### Bug Fixes * session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) ([#8182](https://github.com/parse-community/parse-server/issues/8182)) ([6d0b2f5](https://github.com/parse-community/parse-server/commit/6d0b2f534603301bb630d9c8e497af3bc7ff1d09)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 40eb03a054..21e6994bda 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (2022-09-20) + + +### Bug Fixes + +* session object properties can be updated by foreign user; this fixes a security vulnerability in which a foreign user can write to the session object of another user if the session object ID is known; the fix prevents writing to foreign session objects ([GHSA-6w4q-23cf-j9jp](https://github.com/parse-community/parse-server/security/advisories/GHSA-6w4q-23cf-j9jp)) ([#8182](https://github.com/parse-community/parse-server/issues/8182)) ([6d0b2f5](https://github.com/parse-community/parse-server/commit/6d0b2f534603301bb630d9c8e497af3bc7ff1d09)) + ## [5.2.5](https://github.com/parse-community/parse-server/compare/5.2.4...5.2.5) (2022-09-02) diff --git a/package-lock.json b/package-lock.json index 0ffe4c1645..3a53ec16b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.5", + "version": "5.2.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b574d65d5f..6828040462 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.5", + "version": "5.2.6", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From ecf0814499bde31ab6082b6e42854aa65ad2e03e Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Tue, 20 Sep 2022 22:31:19 +0200 Subject: [PATCH 11/14] fix: authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) (#8185) --- spec/AuthenticationAdapters.spec.js | 23 +++++++++++++++++++++++ src/Adapters/Auth/facebook.js | 19 ++++++++++--------- src/Adapters/Auth/spotify.js | 15 ++++++++------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 5c0123f3b7..e96cb02158 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -441,6 +441,29 @@ describe('AuthenticationProviders', function () { expect(httpsRequest.get.calls.first().args[0].includes('appsecret_proof')).toBe(true); }); + it('should throw error when Facebook request appId is wrong data type', async () => { + const httpsRequest = require('../lib/Adapters/Auth/httpsRequest'); + spyOn(httpsRequest, 'get').and.callFake(() => { + return Promise.resolve({ id: 'a' }); + }); + const options = { + facebook: { + appIds: 'abcd', + appSecret: 'secret_sauce', + }, + }; + const authData = { + access_token: 'badtoken', + }; + const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter( + 'facebook', + options + ); + await expectAsync(adapter.validateAppId(appIds, authData, providerOptions)).toBeRejectedWith( + new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.') + ); + }); + it('should handle Facebook appSecret for validating auth data', async () => { const httpsRequest = require('../lib/Adapters/Auth/httpsRequest'); spyOn(httpsRequest, 'get').and.callFake(() => { diff --git a/src/Adapters/Auth/facebook.js b/src/Adapters/Auth/facebook.js index 95fba0b3e7..e937bb1bb5 100644 --- a/src/Adapters/Auth/facebook.js +++ b/src/Adapters/Auth/facebook.js @@ -32,22 +32,23 @@ function validateGraphToken(authData, options) { }); } -function validateGraphAppId(appIds, authData, options) { +async function validateGraphAppId(appIds, authData, options) { var access_token = authData.access_token; if (process.env.TESTING && access_token === 'test') { - return Promise.resolve(); + return; + } + if (!Array.isArray(appIds)) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.'); } if (!appIds.length) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is not configured.'); } - return graphRequest( - 'app?access_token=' + access_token + getAppSecretPath(authData, options) - ).then(data => { - if (data && appIds.indexOf(data.id) != -1) { - return; - } + const data = await graphRequest( + `app?access_token=${access_token}${getAppSecretPath(authData, options)}` + ); + if (!data || !appIds.includes(data.id)) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is invalid for this user.'); - }); + } } const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => { diff --git a/src/Adapters/Auth/spotify.js b/src/Adapters/Auth/spotify.js index 69e6cbae2d..604868d078 100644 --- a/src/Adapters/Auth/spotify.js +++ b/src/Adapters/Auth/spotify.js @@ -13,17 +13,18 @@ function validateAuthData(authData) { } // Returns a promise that fulfills if this app id is valid. -function validateAppId(appIds, authData) { - var access_token = authData.access_token; +async function validateAppId(appIds, authData) { + const access_token = authData.access_token; + if (!Array.isArray(appIds)) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'appIds must be an array.'); + } if (!appIds.length) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.'); } - return request('me', access_token).then(data => { - if (data && appIds.indexOf(data.id) != -1) { - return; - } + const data = await request('me', access_token); + if (!data || !appIds.includes(data.id)) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is invalid for this user.'); - }); + } } // A promisey wrapper for Spotify API requests. From e6dc487963ceb23354e84851d22d36083c4d19e8 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 20 Sep 2022 20:43:51 +0000 Subject: [PATCH 12/14] chore(release): 5.2.7 [skip ci] ## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (2022-09-20) ### Bug Fixes * authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) ([#8185](https://github.com/parse-community/parse-server/issues/8185)) ([ecf0814](https://github.com/parse-community/parse-server/commit/ecf0814499bde31ab6082b6e42854aa65ad2e03e)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 21e6994bda..769a6d387a 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (2022-09-20) + + +### Bug Fixes + +* authentication adapter app ID validation may be circumvented; this fixes a vulnerability that affects configurations which allow users to authenticate using the Parse Server authentication adapter for *Facebook* or *Spotify* and where the server-side authentication adapter configuration `appIds` is set as a string (e.g. `abc`) instead of an array of strings (e.g. `["abc"]`) ([GHSA-r657-33vp-gp22](https://github.com/parse-community/parse-server/security/advisories/GHSA-r657-33vp-gp22)) ([#8185](https://github.com/parse-community/parse-server/issues/8185)) ([ecf0814](https://github.com/parse-community/parse-server/commit/ecf0814499bde31ab6082b6e42854aa65ad2e03e)) + ## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (2022-09-20) diff --git a/package-lock.json b/package-lock.json index 3a53ec16b6..00fe040bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.6", + "version": "5.2.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6828040462..6973fdd197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.6", + "version": "5.2.7", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { From 066f29673ab4030b6b5b90c0c0326f7d3fe7612a Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sat, 15 Oct 2022 00:48:22 +0200 Subject: [PATCH 13/14] 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)) (#8235) --- spec/ParseFile.spec.js | 209 ++++++++++++++++++++-- src/Adapters/Files/GridFSBucketAdapter.js | 33 ++-- src/Routers/FilesRouter.js | 7 +- 3 files changed, 228 insertions(+), 21 deletions(-) diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 281ce45cc2..88d6953eed 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -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,8 +1070,8 @@ 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', @@ -888,12 +1079,10 @@ describe('Parse.File testing', () => { '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.'); }); }); diff --git a/src/Adapters/Files/GridFSBucketAdapter.js b/src/Adapters/Files/GridFSBucketAdapter.js index 06896e73b5..8ff4d2b8eb 100644 --- a/src/Adapters/Files/GridFSBucketAdapter.js +++ b/src/Adapters/Files/GridFSBucketAdapter.js @@ -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(); diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index bdd86ae59b..a828f7adf4 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -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' + ); } From 8011b2fdace4c25a967c0193dd4d32c2dbd4e4bc Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 14 Oct 2022 22:55:38 +0000 Subject: [PATCH 14/14] chore(release): 5.2.8 [skip ci] ## [5.2.8](https://github.com/parse-community/parse-server/compare/5.2.7...5.2.8) (2022-10-14) ### Bug Fixes * 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](https://github.com/parse-community/parse-server/issues/8235)) ([066f296](https://github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612a)) --- changelogs/CHANGELOG_release.md | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/changelogs/CHANGELOG_release.md b/changelogs/CHANGELOG_release.md index 769a6d387a..9dc05eec33 100644 --- a/changelogs/CHANGELOG_release.md +++ b/changelogs/CHANGELOG_release.md @@ -1,3 +1,10 @@ +## [5.2.8](https://github.com/parse-community/parse-server/compare/5.2.7...5.2.8) (2022-10-14) + + +### Bug Fixes + +* 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](https://github.com/parse-community/parse-server/issues/8235)) ([066f296](https://github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612a)) + ## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (2022-09-20) diff --git a/package-lock.json b/package-lock.json index 00fe040bf2..ad5a608794 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.7", + "version": "5.2.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6973fdd197..ba700337a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.2.7", + "version": "5.2.8", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": {