CVE-2022-31083 (JavaScript, published 2022-06-17T19:15Z)
Description: value: 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.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-295 \u2014 Improper Certificate Validation
The product does not validate, or incorrectly validates, a certificate.
Repository: https://github.com/parse-community/parse-server
Files changed:
release.config.js
spec/AuthenticationAdapters.spec.js
spec/support/cert/game_center.pem
src/Adapters/Auth/gcenter.js
Commit message: 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))



Vulnerable code:
            { from: "release", to: "alpha" },
      ['@semantic-release/github', {
        successComment: getReleaseComment(),
        labels: ['type:ci'],
        releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
      }],


      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer',
      bundleId: 'cloud.xtralife.gamecenterauth',

    try {
      await gcenter.validateAuthData(authData);
      fail();
    } catch (e) {
      expect(e.message).toBe('Apple Game Center - invalid signature');
    }

  const headers = await new Promise((resolve, reject) =>
    headers['content-type'] !== 'application/pkix-cert' ||
    headers['content-length'] == null ||
    headers['content-length'] > 10000
      .get(publicKeyUrl, res => {
        let data = '';
          data += chunk.toString('base64');
          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);
            }
          resolve(cert);
function validateAppId() {
  return Promise.resolve();

Fixed code:
      ['@semantic-release/github', {
        successComment: getReleaseComment(),
        labels: ['type:ci'],
        releasedLabels: ['state:released<%= nextRelease.channel ? `-\${nextRelease.channel}` : "" %>']
      }],
  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
    );
  });
    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
    );
    gcenter.cache['https://static.gc.apple.com/public-key/gc-prod-4.cer'] = testCert;
    const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
      'gcenter',
      {}
    );
    await adapter.validateAppId(
      appIds,
      { publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
      providerOptions
    );
      publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
      bundleId: 'com.example.com',
    await expectAsync(gcenter.validateAuthData(authData)).toBeRejectedWith(
      new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Apple Game Center - invalid signature')
    );
    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('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.'
      )
    );
  });
-----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-----
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
  const cert_headers = await new Promise((resolve, reject) =>
  const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
    !validContentTypes.includes(cert_headers['content-type']) ||
    cert_headers['content-length'] == null ||
    cert_headers['content-length'] > 10000
  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) {
      .get(url, res => {
        const data = [];
          data.push(chunk);
          if (buffer) {
            resolve({ certificate: Buffer.concat(data), headers: res.headers });
            return;
          let cert = '';
          for (const chunk of data) {
            cert += chunk.toString('base64');
          }
          const certificate = convertX509CertToPEM(cert);
          resolve({ certificate, headers: res.headers });
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;
}

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;
  cache,