From 4a1039679c1dfb60bf3ff16fb5b9644f96aee10a Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Jun 2022 19:31:49 +0200 Subject: [PATCH 1/2] docs: add release instructions (#8056) --- CONTRIBUTING.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8278ed91dc..6cb8ef9f7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,6 +27,10 @@ - [Releasing](#releasing) - [General Considerations](#general-considerations) - [Major Release / Long-Term-Support](#major-release--long-term-support) + - [Preparing Release](#preparing-release) + - [Publishing Release (forward-merge):](#publishing-release-forward-merge) + - [Publishing Hotfix (back-merge):](#publishing-hotfix-back-merge) + - [Publishing Major Release (Yearly Release)](#publishing-major-release-yearly-release) - [Versioning](#versioning) - [Code of Conduct](#code-of-conduct) @@ -385,20 +389,66 @@ If the commit reverts a previous commit, use the prefix `revert:`, followed by t ### General Considerations -- The `package-lock.json` file has to be deleted and recreated by npm from scratch in regular intervals using the `npm i` command. It is not enough to only update the file via automated security pull requests (e.g. dependabot, snyk), that can create inconsistencies between sub-devependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly. +- The `package-lock.json` file has to be deleted and recreated by npm from scratch in regular intervals using the `npm i` command. It is not enough to only update the file via automated security pull requests (e.g. dependabot, snyk), that can create inconsistencies between sub-dependencies of a dependency and increase the chances of vulnerabilities. The file should be recreated once every release cycle which is usually monthly. ### Major Release / Long-Term-Support Long-Term-Support (LTS) is provided for the previous Parse Server major version. For example, Parse Server 4.x will receive security updates until Parse Server 5.x is superseded by Parse Server 6.x and becomes the new LTS version. While the current major version is published on branch `release`, a LTS version is published on branch `release-#.x.x`, for example `release-4.x.x` for the Parse Server 4.x LTS branch. -#### Preparing Release +### Preparing Release The following changes are done in the `alpha` branch, before publishing the last `beta` version that will eventually become the major release. This way the changes trickle naturally through all branches and code consistency is ensured among branches. - Make sure all [deprecations](https://github.com/parse-community/parse-server/blob/alpha/DEPRECATIONS.md) are reflected in code, old code is removed and the deprecations table is updated. - Add the future LTS branch `release-#.x.x` to the branch list in [release.config.js](https://github.com/parse-community/parse-server/blob/alpha/release.config.js) so that the branch will later be recognized for release automation. -#### Publishing Release + +### Publishing Release (forward-merge): + +1. Create new temporary branch `build` on branch `beta`. +2. Create PR to merge `build` into `release`: + - PR title: `build: release` + - PR description: (leave empty) +3. Resolve any conflicts: + - For conflicts regarding the package version in `package.json` and `package-lock.json` it doesn't matter which version is chosen, as the version will be set by auto-release in a commit after merging. However, for both files the same version should be chosen when resolving the conflict. +4. Merge PR with a "merge commit", do not "squash and merge": + - Commit message: (use PR title) + - Description: (leave empty) +5. Wait for GitHub Action `release-automated` to finish: + - If GitHub Action fails, investigate why; manual correction may be needed. +6. Pull all remote branches into local branches. +7. Delete temporary branch `build`. +8. Create new temporary branch `build` on branch `alpha`. +9. Create PR to merge `build` into `beta`: + - PR title: `build: release` + - PR description: (leave empty) +8. Repeat steps 3-7 for PR from step 9. + +### Publishing Hotfix (back-merge): + +1. Create PR to merge hotfix PR into `release`: + - Merge PR following the same rules as any PR would be merged into the working branch `alpha`. +2. Wait for GitHub Action `release-automated` to finish: + - GitHub Action will fail with error `! [rejected] HEAD -> beta (non-fast-forward)`; this is expected as auto-release currently cannot fully handle back-merging; docker will not publish the new release, so this has to be done manually using the GitHub workflow `release-manual-docker` and entering the version tag that has been created by auto-release. +3. Pull all remote branches into local branches. +4. Create a new temporary branch `backmerge` on branch `release`. +5. Create PR to merge `backmerge` into `beta`: + - PR title: ` [skip release]` where `` is the PR title of step 1. + - PR description: (leave empty) +6. Resolve any conflicts: + - During back-merging, usually all changes are preserved; current changes come from the hotfix in the `release` branch, the incoming changes come from the `beta` branch usually being ahead of the `release` branch. This makes back-merging so complex and bug-prone and is the main reason why it should be avoided if possible. +7. Merge PR with "squash and merge", do not do a "merge commit": + - Commit message: (use PR title) + - Description: (leave empty) + + ℹ️ Merging this PR will not trigger a release; the back-merge will not appear in changelogs of the `beta`, `alpha` branches; the back-merged fix will be an undocumented change of these branches' next releases; if necessary, the change needs to be added manually to the pre-release changelogs *after* the next pre-releases. +8. Delete temporary branch `backmerge`. +10. Create a new temporary branch `backmerge` on branch `beta`. +11. Repeat steps 4-8 to merge PR into `alpha`. + +⚠️ Long-term-support branches are excluded from the processes above and handled individually as they do not have pre-releases branches and are not considered part of the current codebase anymore. It may be necessary to significantly adapt a PR for a LTS branch due to the differences in codebase and CI tests. This adaption should be done in advance before merging any related PR, especially for security fixes, as to not publish a vulnerability while it may still take significant time to adapt the fix for the older codebase of a LTS branch. + +### Publishing Major Release (Yearly Release) 1. Create LTS branch `release-#.x.x` off the latest version tag on `release` branch. 2. Create temporary branch `build-release` off branch `beta` and create a pull request with `release` as the base branch. From 75af9a26cc8e9e88a33d1e452c93a0ee6e509f17 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 17 Jun 2022 20:22:35 +0200 Subject: [PATCH 2/2] fix: certificate in Apple Game Center auth adapter not validated [skip release] (#8058) --- changelogs/CHANGELOG_release.md | 7 ++ package-lock.json | 2 +- package.json | 2 +- spec/AuthenticationAdapters.spec.js | 130 ++++++++++++++++++++++++++++ src/Adapters/Auth/gcenter.js | 100 ++++++++++++++++----- 5 files changed, 218 insertions(+), 23 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 ec12d05b03..5e51b515a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.3.0-beta.1", + "version": "5.2.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6c5362e92f..26402c479f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server", - "version": "5.3.0-beta.1", + "version": "5.2.2", "description": "An express module providing a Parse-compatible API server", "main": "lib/index.js", "repository": { diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 6587afc239..195d899819 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1682,7 +1682,41 @@ 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', @@ -1698,6 +1732,15 @@ describe('Apple Game Center Auth adapter', () => { }); 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-6.cer', @@ -1712,6 +1755,21 @@ describe('Apple Game Center Auth adapter', () => { }); 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', @@ -1739,6 +1797,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/src/Adapters/Auth/gcenter.js b/src/Adapters/Auth/gcenter.js index 19ff5d1832..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,8 +165,27 @@ 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 = {