Skip to content

Commit 58ba161

Browse files
authored
Merge 0849910 into 4a10396
2 parents 4a10396 + 0849910 commit 58ba161

File tree

5 files changed

+218
-23
lines changed

5 files changed

+218
-23
lines changed

changelogs/CHANGELOG_release.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [5.2.2](https://github.com/parse-community/parse-server/compare/5.2.1...5.2.2) (2022-06-17)
2+
3+
4+
### Bug Fixes
5+
6+
* 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))
7+
18
## [5.2.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1) (2022-05-01)
29

310

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "5.3.0-beta.1",
3+
"version": "5.2.2",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

spec/AuthenticationAdapters.spec.js

+130
Original file line numberDiff line numberDiff line change
@@ -1682,7 +1682,41 @@ describe('Apple Game Center Auth adapter', () => {
16821682
const gcenter = require('../lib/Adapters/Auth/gcenter');
16831683
const fs = require('fs');
16841684
const testCert = fs.readFileSync(__dirname + '/support/cert/game_center.pem');
1685+
1686+
it('can load adapter', async () => {
1687+
const options = {
1688+
gcenter: {
1689+
rootCertificateUrl:
1690+
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
1691+
},
1692+
};
1693+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1694+
'gcenter',
1695+
options
1696+
);
1697+
await adapter.validateAppId(
1698+
appIds,
1699+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1700+
providerOptions
1701+
);
1702+
});
1703+
16851704
it('validateAuthData should validate', async () => {
1705+
const options = {
1706+
gcenter: {
1707+
rootCertificateUrl:
1708+
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
1709+
},
1710+
};
1711+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1712+
'gcenter',
1713+
options
1714+
);
1715+
await adapter.validateAppId(
1716+
appIds,
1717+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1718+
providerOptions
1719+
);
16861720
// real token is used
16871721
const authData = {
16881722
id: 'G:1965586982',
@@ -1698,6 +1732,15 @@ describe('Apple Game Center Auth adapter', () => {
16981732
});
16991733

17001734
it('validateAuthData invalid signature id', async () => {
1735+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1736+
'gcenter',
1737+
{}
1738+
);
1739+
await adapter.validateAppId(
1740+
appIds,
1741+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1742+
providerOptions
1743+
);
17011744
const authData = {
17021745
id: 'G:1965586982',
17031746
publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-6.cer',
@@ -1712,6 +1755,21 @@ describe('Apple Game Center Auth adapter', () => {
17121755
});
17131756

17141757
it('validateAuthData invalid public key http url', async () => {
1758+
const options = {
1759+
gcenter: {
1760+
rootCertificateUrl:
1761+
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
1762+
},
1763+
};
1764+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1765+
'gcenter',
1766+
options
1767+
);
1768+
await adapter.validateAppId(
1769+
appIds,
1770+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1771+
providerOptions
1772+
);
17151773
const publicKeyUrls = [
17161774
'example.com',
17171775
'http://static.gc.apple.com/public-key/gc-prod-4.cer',
@@ -1739,6 +1797,78 @@ describe('Apple Game Center Auth adapter', () => {
17391797
)
17401798
);
17411799
});
1800+
1801+
it('should not validate Symantec Cert', async () => {
1802+
const options = {
1803+
gcenter: {
1804+
rootCertificateUrl:
1805+
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem',
1806+
},
1807+
};
1808+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1809+
'gcenter',
1810+
options
1811+
);
1812+
await adapter.validateAppId(
1813+
appIds,
1814+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1815+
providerOptions
1816+
);
1817+
expect(() =>
1818+
gcenter.verifyPublicKeyIssuer(
1819+
testCert,
1820+
'https://static.gc.apple.com/public-key/gc-prod-4.cer'
1821+
)
1822+
);
1823+
});
1824+
1825+
it('adapter should load default cert', async () => {
1826+
const options = {
1827+
gcenter: {},
1828+
};
1829+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1830+
'gcenter',
1831+
options
1832+
);
1833+
await adapter.validateAppId(
1834+
appIds,
1835+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1836+
providerOptions
1837+
);
1838+
const previous = new Date();
1839+
await adapter.validateAppId(
1840+
appIds,
1841+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1842+
providerOptions
1843+
);
1844+
1845+
const duration = new Date().getTime() - previous.getTime();
1846+
expect(duration).toEqual(0);
1847+
});
1848+
1849+
it('adapter should throw', async () => {
1850+
const options = {
1851+
gcenter: {
1852+
rootCertificateUrl: 'https://example.com',
1853+
},
1854+
};
1855+
const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter(
1856+
'gcenter',
1857+
options
1858+
);
1859+
await expectAsync(
1860+
adapter.validateAppId(
1861+
appIds,
1862+
{ publicKeyUrl: 'https://static.gc.apple.com/public-key/gc-prod-4.cer' },
1863+
providerOptions
1864+
)
1865+
).toBeRejectedWith(
1866+
new Parse.Error(
1867+
Parse.Error.OBJECT_NOT_FOUND,
1868+
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
1869+
)
1870+
);
1871+
});
17421872
});
17431873

17441874
describe('phant auth adapter', () => {

src/Adapters/Auth/gcenter.js

+79-21
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ const authData = {
1414
const { Parse } = require('parse/node');
1515
const crypto = require('crypto');
1616
const https = require('https');
17-
17+
const { pki } = require('node-forge');
18+
const ca = { cert: null, url: null };
1819
const cache = {}; // (publicKey -> cert) cache
1920

2021
function verifyPublicKeyUrl(publicKeyUrl) {
@@ -52,39 +53,53 @@ async function getAppleCertificate(publicKeyUrl) {
5253
path: url.pathname,
5354
method: 'HEAD',
5455
};
55-
const headers = await new Promise((resolve, reject) =>
56+
const cert_headers = await new Promise((resolve, reject) =>
5657
https.get(headOptions, res => resolve(res.headers)).on('error', reject)
5758
);
59+
const validContentTypes = ['application/x-x509-ca-cert', 'application/pkix-cert'];
5860
if (
59-
headers['content-type'] !== 'application/pkix-cert' ||
60-
headers['content-length'] == null ||
61-
headers['content-length'] > 10000
61+
!validContentTypes.includes(cert_headers['content-type']) ||
62+
cert_headers['content-length'] == null ||
63+
cert_headers['content-length'] > 10000
6264
) {
6365
throw new Parse.Error(
6466
Parse.Error.OBJECT_NOT_FOUND,
6567
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
6668
);
6769
}
70+
const { certificate, headers } = await getCertificate(publicKeyUrl);
71+
if (headers['cache-control']) {
72+
const expire = headers['cache-control'].match(/max-age=([0-9]+)/);
73+
if (expire) {
74+
cache[publicKeyUrl] = certificate;
75+
// we'll expire the cache entry later, as per max-age
76+
setTimeout(() => {
77+
delete cache[publicKeyUrl];
78+
}, parseInt(expire[1], 10) * 1000);
79+
}
80+
}
81+
return verifyPublicKeyIssuer(certificate, publicKeyUrl);
82+
}
83+
84+
function getCertificate(url, buffer) {
6885
return new Promise((resolve, reject) => {
6986
https
70-
.get(publicKeyUrl, res => {
71-
let data = '';
87+
.get(url, res => {
88+
const data = [];
7289
res.on('data', chunk => {
73-
data += chunk.toString('base64');
90+
data.push(chunk);
7491
});
7592
res.on('end', () => {
76-
const cert = convertX509CertToPEM(data);
77-
if (res.headers['cache-control']) {
78-
var expire = res.headers['cache-control'].match(/max-age=([0-9]+)/);
79-
if (expire) {
80-
cache[publicKeyUrl] = cert;
81-
// we'll expire the cache entry later, as per max-age
82-
setTimeout(() => {
83-
delete cache[publicKeyUrl];
84-
}, parseInt(expire[1], 10) * 1000);
85-
}
93+
if (buffer) {
94+
resolve({ certificate: Buffer.concat(data), headers: res.headers });
95+
return;
8696
}
87-
resolve(cert);
97+
let cert = '';
98+
for (const chunk of data) {
99+
cert += chunk.toString('base64');
100+
}
101+
const certificate = convertX509CertToPEM(cert);
102+
resolve({ certificate, headers: res.headers });
88103
});
89104
})
90105
.on('error', reject);
@@ -115,6 +130,30 @@ function verifySignature(publicKey, authData) {
115130
}
116131
}
117132

133+
function verifyPublicKeyIssuer(cert, publicKeyUrl) {
134+
const publicKeyCert = pki.certificateFromPem(cert);
135+
if (!ca.cert) {
136+
throw new Parse.Error(
137+
Parse.Error.OBJECT_NOT_FOUND,
138+
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
139+
);
140+
}
141+
try {
142+
if (!ca.cert.verify(publicKeyCert)) {
143+
throw new Parse.Error(
144+
Parse.Error.OBJECT_NOT_FOUND,
145+
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
146+
);
147+
}
148+
} catch (e) {
149+
throw new Parse.Error(
150+
Parse.Error.OBJECT_NOT_FOUND,
151+
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
152+
);
153+
}
154+
return cert;
155+
}
156+
118157
// Returns a promise that fulfills if this user id is valid.
119158
async function validateAuthData(authData) {
120159
if (!authData.id) {
@@ -126,8 +165,27 @@ async function validateAuthData(authData) {
126165
}
127166

128167
// Returns a promise that fulfills if this app id is valid.
129-
function validateAppId() {
130-
return Promise.resolve();
168+
async function validateAppId(appIds, authData, options = {}) {
169+
if (!options.rootCertificateUrl) {
170+
options.rootCertificateUrl =
171+
'https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt.pem';
172+
}
173+
if (ca.url === options.rootCertificateUrl) {
174+
return;
175+
}
176+
const { certificate, headers } = await getCertificate(options.rootCertificateUrl, true);
177+
if (
178+
headers['content-type'] !== 'application/x-pem-file' ||
179+
headers['content-length'] == null ||
180+
headers['content-length'] > 10000
181+
) {
182+
throw new Parse.Error(
183+
Parse.Error.OBJECT_NOT_FOUND,
184+
'Apple Game Center auth adapter parameter `rootCertificateURL` is invalid.'
185+
);
186+
}
187+
ca.cert = pki.certificateFromPem(certificate);
188+
ca.url = options.rootCertificateUrl;
131189
}
132190

133191
module.exports = {

0 commit comments

Comments
 (0)