Authentication bypass in Parse Server Apple Game Center auth adapter
Description
Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Prior to versions 4.10.11 and 5.2.2, the certificate in the Parse Server Apple Game Center auth adapter not validated. As a result, authentication could potentially be bypassed by making a fake certificate accessible via certain Apple domains and providing the URL to that certificate in an authData object. Versions 4.0.11 and 5.2.2 prevent this by introducing a new rootCertificateUrl property to the Parse Server Apple Game Center auth adapter which takes the URL to the root certificate of Apple's Game Center authentication certificate. If no value is set, the rootCertificateUrl property defaults to the URL of the current root certificate as of May 27, 2022. Keep in mind that the root certificate can change at any time and that it is the developer's responsibility to keep the root certificate URL up-to-date when using the Parse Server Apple Game Center auth adapter. There are no known workarounds for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
parse-servernpm | < 4.10.11 | 4.10.11 |
parse-servernpm | >= 5.0.0, < 5.2.2 | 5.2.2 |
Affected products
1- Range: < 4.0.11
Patches
1ba2b0a9cb9a5fix: 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))
4 files changed · +249 −38
release.config.js+5 −6 modified@@ -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}` : "" %>'] - }], ], };
spec/AuthenticationAdapters.spec.js+136 −11 modified@@ -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', () => {
spec/support/cert/game_center.pem+28 −0 added@@ -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-----
src/Adapters/Auth/gcenter.js+80 −21 modified@@ -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, };
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
8- github.com/advisories/GHSA-rh9j-f5f8-rvgcghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31083ghsaADVISORY
- developer.apple.com/news/ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/commit/ba2b0a9cb9a568817a114b132a4c2e0911d76df1ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/8054ghsax_refsource_MISCWEB
- github.com/parse-community/parse-server/pull/8054/commits/0cc299f82e367518f2fe7a53b99f3f801a338cf4ghsaWEB
- github.com/parse-community/parse-server/pull/8054/commits/2084b7c569697a5230e42511799eeac9219db5a9ghsaWEB
- github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgcghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.