Compare commits

..
22 Commits
Author SHA1 Message Date
semantic-release-bot 35346525e8 chore(release): 4.10.18 [skip ci]
## [4.10.18](https://github.com/parse-community/parse-server/compare/4.10.17...4.10.18) (2022-11-07)

### Bug Fixes

* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) ([#8296](https://github.com/parse-community/parse-server/issues/8296)) ([47cfeee](https://github.com/parse-community/parse-server/commit/47cfeee0ce56a16c6783083acd2cbda2c2bd844d))
2022-11-07 22:24:24 +00:00
Manuel 47cfeee0ce fix: Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) (#8296) 2022-11-07 23:05:29 +01:00
semantic-release-bot 041e60487c chore(release): 4.10.17 [skip ci]
## [4.10.17](https://github.com/parse-community/parse-server/compare/4.10.16...4.10.17) (2022-10-15)

### 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)) ([#8236](https://github.com/parse-community/parse-server/issues/8236)) ([3d7a61e](https://github.com/parse-community/parse-server/commit/3d7a61ecd5231638f01ff1a965b6313043c594a7))
2022-10-15 00:25:57 +00:00
Manuel 3d7a61ecd5 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)) (#8236) 2022-10-15 02:12:05 +02:00
semantic-release-bot f03bf00e5f chore(release): 4.10.16 [skip ci]
## [4.10.16](https://github.com/parse-community/parse-server/compare/4.10.15...4.10.16) (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)) ([#8186](https://github.com/parse-community/parse-server/issues/8186)) ([b3e7939](https://github.com/parse-community/parse-server/commit/b3e7939f6bf7d4d295143c5d9b90deb9c25c58b7))
2022-09-20 20:56:54 +00:00
Manuel b3e7939f6b 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)) (#8186) 2022-09-20 22:32:19 +02:00
semantic-release-bot 9d502269c5 chore(release): 4.10.15 [skip ci]
## [4.10.15](https://github.com/parse-community/parse-server/compare/4.10.14...4.10.15) (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)) ([#8183](https://github.com/parse-community/parse-server/issues/8183)) ([7ca9ed0](https://github.com/parse-community/parse-server/commit/7ca9ed01424478d299e5576ee4208bd9fea78760))
2022-09-20 00:33:55 +00:00
Manuel 7ca9ed0142 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)) (#8183) 2022-09-20 02:19:43 +02:00
semantic-release-bot e29f7c0431 chore(release): 4.10.14 [skip ci]
## [4.10.14](https://github.com/parse-community/parse-server/compare/4.10.13...4.10.14) (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) ([#8143](https://github.com/parse-community/parse-server/issues/8143)) ([634c44a](https://github.com/parse-community/parse-server/commit/634c44acd18f6ee6ec60fac89a2b602d92799bec))
2022-09-02 19:28:55 +00:00
Manuel 634c44acd1 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) (#8143) 2022-09-02 21:15:09 +02:00
semantic-release-bot 4748e9bbd3 chore(release): 4.10.13 [skip ci]
## [4.10.13](https://github.com/parse-community/parse-server/compare/4.10.12...4.10.13) (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)) ([#8074](https://github.com/parse-community/parse-server/issues/8074)) ([054f3e6](https://github.com/parse-community/parse-server/commit/054f3e6ab01d66a0dcfb77725af28eac1485b375))
2022-06-30 10:38:27 +00:00
Manuel 054f3e6ab0 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)) (#8074) 2022-06-30 12:24:34 +02:00
semantic-release-bot 6286d2e34f chore(release): 4.10.12 [skip ci]
## [4.10.12](https://github.com/parse-community/parse-server/compare/4.10.11...4.10.12) (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)) ([#8059](https://github.com/parse-community/parse-server/issues/8059)) ([5f42322](https://github.com/parse-community/parse-server/commit/5f423224bda8df32affc4ed829eaddd87e4cb622))
2022-06-17 23:43:36 +00:00
Manuel 5f423224bd 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)) (#8059) 2022-06-18 01:29:49 +02:00
semantic-release-bot ad680bd312 chore(release): 4.10.11 [skip ci]
## [4.10.11](https://github.com/parse-community/parse-server/compare/4.10.10...4.10.11) (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)) ([145838d](https://github.com/parse-community/parse-server/commit/145838d2d9c1ecf76412a962a4ef61c712bcb0a7))
2022-06-17 16:38:16 +00:00
Manuel 145838d2d9 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)) 2022-06-17 18:24:13 +02:00
Manuel Trezza 8580a524eb fix CI timeout 2022-06-17 17:57:22 +02:00
Manuel Trezza 53afafa13f Update gcenter.js 2022-06-17 17:30:54 +02:00
Manuel Trezza c411c48d49 Create game_center.pem 2022-06-17 16:19:25 +02:00
Manuel Trezza 07786c1666 fix adapter 2022-06-17 16:19:25 +02:00
semantic-release-bot b00b0410e6 chore(release): 4.10.10 [skip ci]
## [4.10.10](https://github.com/parse-community/parse-server/compare/4.10.9...4.10.10) (2022-05-01)

### Bug Fixes

* authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) ([#7963](https://github.com/parse-community/parse-server/issues/7963)) ([1930a64](https://github.com/parse-community/parse-server/commit/1930a64e9c7e0d287b4ad9e57c6ee03b11b9cbc4))
2022-05-01 00:48:05 +00:00
Manuel 1930a64e9c fix: authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) (#7963) 2022-05-01 02:46:57 +02:00
22 changed files with 1039 additions and 164 deletions
+5
View File
@@ -13,6 +13,7 @@ env:
jobs:
check-mongo:
strategy:
fail-fast: false
matrix:
include:
- name: Mongo 4.0.4, ReplicaSet, WiredTiger
@@ -43,6 +44,8 @@ jobs:
PARSE_SERVER_TEST_CACHE: ${{ matrix.PARSE_SERVER_TEST_CACHE }}
NODE_VERSION: ${{ matrix.NODE_VERSION }}
steps:
- name: Fix usage of insecure GitHub protocol
run: sudo git config --system url."https://github".insteadOf "git://github"
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.NODE_VERSION }}
uses: actions/setup-node@v1
@@ -89,6 +92,8 @@ jobs:
POSTGRES_MAJOR_VERSION: 11
PARSE_SERVER_TEST_DATABASE_URI: postgres://postgres:postgres@localhost:5432/parse_server_postgres_adapter_test_database
steps:
- name: Fix usage of insecure GitHub protocol
run: sudo git config --system url."https://github".insteadOf "git://github"
- uses: actions/checkout@v2
- name: Use Node.js 10
uses: actions/setup-node@v1
+63
View File
@@ -1,3 +1,66 @@
## [4.10.18](https://github.com/parse-community/parse-server/compare/4.10.17...4.10.18) (2022-11-07)
### Bug Fixes
* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-prm5-8g2m-24gg](https://github.com/parse-community/parse-server/security/advisories/GHSA-prm5-8g2m-24gg) ([#8296](https://github.com/parse-community/parse-server/issues/8296)) ([47cfeee](https://github.com/parse-community/parse-server/commit/47cfeee0ce56a16c6783083acd2cbda2c2bd844d))
## [4.10.17](https://github.com/parse-community/parse-server/compare/4.10.16...4.10.17) (2022-10-15)
### 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)) ([#8236](https://github.com/parse-community/parse-server/issues/8236)) ([3d7a61e](https://github.com/parse-community/parse-server/commit/3d7a61ecd5231638f01ff1a965b6313043c594a7))
## [4.10.16](https://github.com/parse-community/parse-server/compare/4.10.15...4.10.16) (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)) ([#8186](https://github.com/parse-community/parse-server/issues/8186)) ([b3e7939](https://github.com/parse-community/parse-server/commit/b3e7939f6bf7d4d295143c5d9b90deb9c25c58b7))
## [4.10.15](https://github.com/parse-community/parse-server/compare/4.10.14...4.10.15) (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)) ([#8183](https://github.com/parse-community/parse-server/issues/8183)) ([7ca9ed0](https://github.com/parse-community/parse-server/commit/7ca9ed01424478d299e5576ee4208bd9fea78760))
## [4.10.14](https://github.com/parse-community/parse-server/compare/4.10.13...4.10.14) (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) ([#8143](https://github.com/parse-community/parse-server/issues/8143)) ([634c44a](https://github.com/parse-community/parse-server/commit/634c44acd18f6ee6ec60fac89a2b602d92799bec))
## [4.10.13](https://github.com/parse-community/parse-server/compare/4.10.12...4.10.13) (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)) ([#8074](https://github.com/parse-community/parse-server/issues/8074)) ([054f3e6](https://github.com/parse-community/parse-server/commit/054f3e6ab01d66a0dcfb77725af28eac1485b375))
## [4.10.12](https://github.com/parse-community/parse-server/compare/4.10.11...4.10.12) (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)) ([#8059](https://github.com/parse-community/parse-server/issues/8059)) ([5f42322](https://github.com/parse-community/parse-server/commit/5f423224bda8df32affc4ed829eaddd87e4cb622))
## [4.10.11](https://github.com/parse-community/parse-server/compare/4.10.10...4.10.11) (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)) ([145838d](https://github.com/parse-community/parse-server/commit/145838d2d9c1ecf76412a962a4ef61c712bcb0a7))
## [4.10.10](https://github.com/parse-community/parse-server/compare/4.10.9...4.10.10) (2022-05-01)
### Bug Fixes
* authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) ([#7963](https://github.com/parse-community/parse-server/issues/7963)) ([1930a64](https://github.com/parse-community/parse-server/commit/1930a64e9c7e0d287b4ad9e57c6ee03b11b9cbc4))
## [4.10.9](https://github.com/parse-community/parse-server/compare/4.10.8...4.10.9) (2022-03-28)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "4.10.9",
"version": "4.10.18",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "4.10.9",
"version": "4.10.18",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
+185 -30
View File
@@ -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(() => {
@@ -1644,8 +1667,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',
@@ -1656,48 +1712,147 @@ describe('Apple Game Center Auth adapter', () => {
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
};
try {
await gcenter.validateAuthData(authData);
} catch (e) {
fail();
}
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 url', async () => {
const authData = {
id: 'G:1965586982',
publicKeyUrl: 'invalid.com',
timestamp: 1565257031287,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'cloud.xtralife.gamecenterauth',
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',
'https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg',
'https://example.com/ \\.apple.com/public_key.cer',
'https://example.com/ &.apple.com/public_key.cer',
];
await Promise.all(
publicKeyUrls.map(publicKeyUrl =>
expectAsync(
gcenter.validateAuthData({
id: 'G:1965586982',
timestamp: 1565257031287,
publicKeyUrl,
signature: '1234',
salt: 'DzqqrQ==',
bundleId: 'com.example.com',
})
).toBeRejectedWith(
new Parse.Error(
Parse.Error.SCRIPT_FAILED,
`Apple Game Center - invalid publicKeyUrl: ${publicKeyUrl}`
)
)
)
);
});
try {
await gcenter.validateAuthData(authData);
fail();
} catch (e) {
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
}
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.'
)
);
});
});
+237 -10
View File
@@ -623,7 +623,236 @@ describe('Parse.File testing', () => {
});
});
xdescribe('Gridstore Range tests', () => {
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' });
});
});
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',
@@ -712,7 +941,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',
@@ -810,8 +1039,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',
@@ -819,12 +1048,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.');
});
});
+46
View File
@@ -974,6 +974,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();
+28
View File
@@ -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);
});
});
+4 -4
View File
@@ -378,7 +378,7 @@ describe_only(() => {
const query = new Parse.Query(TestObject);
await query.get(object.id);
expect(getSpy.calls.count()).toBe(3);
expect(getSpy.calls.count()).toBe(4);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(2);
@@ -397,7 +397,7 @@ describe_only(() => {
const query = new Parse.Query(TestObject);
await query.get(object.id);
expect(getSpy.calls.count()).toBe(2);
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(1);
@@ -420,7 +420,7 @@ describe_only(() => {
query.include('child');
await query.get(object.id);
expect(getSpy.calls.count()).toBe(4);
expect(getSpy.calls.count()).toBe(6);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(3);
@@ -444,7 +444,7 @@ describe_only(() => {
expect(objects.length).toBe(1);
expect(objects[0].id).toBe(child.id);
expect(getSpy.calls.count()).toBe(2);
expect(getSpy.calls.count()).toBe(3);
expect(putSpy.calls.count()).toBe(1);
expect(delSpy.calls.count()).toBe(3);
+73
View File
@@ -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' })
+28
View File
@@ -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-----
+38
View File
@@ -279,6 +279,44 @@ describe('Vulnerabilities', () => {
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
expect(text.error).toBe('Prohibited keyword in request data: {"value":"aValue[123]*"}.');
});
it('denies BSON type code data in file metadata', async () => {
const str = 'Hello World!';
const data = [];
for (let i = 0; i < str.length; i++) {
data.push(str.charCodeAt(i));
}
const file = new Parse.File('hello.txt', data, 'text/plain');
file.addMetadata('obj', {
_bsontype: 'Code',
code: 'delete Object.prototype.evalFunctions',
});
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
)
);
});
it('denies BSON type code data in file tags', async () => {
const str = 'Hello World!';
const data = [];
for (let i = 0; i < str.length; i++) {
data.push(str.charCodeAt(i));
}
const file = new Parse.File('hello.txt', data, 'text/plain');
file.addTag('obj', {
_bsontype: 'Code',
code: 'delete Object.prototype.evalFunctions',
});
await expectAsync(file.save()).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.`
)
);
});
});
describe('Ignore non-matches', () => {
+10 -9
View File
@@ -29,22 +29,23 @@ function validateAuthData(authData, options) {
}
// Returns a promise that fulfills iff this app id is valid.
function validateAppId(appIds, authData, options) {
async function validateAppId(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.');
});
}
}
// A promisey wrapper for FB graph requests.
+100 -26
View File
@@ -14,20 +14,17 @@ const authData = {
const { Parse } = require('parse/node');
const crypto = require('crypto');
const https = require('https');
const url = require('url');
const { pki } = require('node-forge');
const ca = { cert: null, url: null };
const cache = {}; // (publicKey -> cert) cache
function verifyPublicKeyUrl(publicKeyUrl) {
const parsedUrl = url.parse(publicKeyUrl);
if (parsedUrl.protocol !== 'https:') {
try {
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
return regex.test(publicKeyUrl);
} catch (error) {
return false;
}
const hostnameParts = parsedUrl.hostname.split('.');
const length = hostnameParts.length;
const domainParts = hostnameParts.slice(length - 2, length);
const domain = domainParts.join('.');
return domain === 'apple.com';
}
function convertX509CertToPEM(X509Cert) {
@@ -40,7 +37,7 @@ function convertX509CertToPEM(X509Cert) {
return pemPreFix + certBody + pemPostFix;
}
function getAppleCertificate(publicKeyUrl) {
async function getAppleCertificate(publicKeyUrl) {
if (!verifyPublicKeyUrl(publicKeyUrl)) {
throw new Parse.Error(
Parse.Error.OBJECT_NOT_FOUND,
@@ -50,26 +47,59 @@ function getAppleCertificate(publicKeyUrl) {
if (cache[publicKeyUrl]) {
return cache[publicKeyUrl];
}
const url = new URL(publicKeyUrl);
const headOptions = {
hostname: url.hostname,
path: url.pathname,
method: 'HEAD',
};
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 (
!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);
@@ -100,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) {
@@ -111,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,
};
+8 -7
View File
@@ -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.
+23 -10
View File
@@ -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();
+45 -41
View File
@@ -51,31 +51,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.');
}
@@ -83,7 +79,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.');
}
@@ -91,7 +87,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,
@@ -111,7 +107,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}`);
}
});
@@ -123,7 +123,7 @@ const filterSensitiveData = (
aclGroup: any[],
auth: any,
operation: any,
schema: SchemaController.SchemaController,
schema: SchemaController.SchemaController | any,
className: string,
protectedFields: null | Array<any>,
object: any
@@ -132,7 +132,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;
@@ -203,27 +204,25 @@ 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;
@@ -512,7 +511,7 @@ class DatabaseController {
if (acl) {
query = addWriteACL(query, acl);
}
validateQuery(query);
validateQuery(query, isMaster, true);
return schemaController
.getOneSchema(className, true)
.catch(error => {
@@ -758,7 +757,7 @@ class DatabaseController {
if (acl) {
query = addWriteACL(query, acl);
}
validateQuery(query);
validateQuery(query, isMaster, false);
return schemaController
.getOneSchema(className)
.catch(error => {
@@ -1231,7 +1230,7 @@ class DatabaseController {
query = addReadACL(query, aclGroup);
}
}
validateQuery(query);
validateQuery(query, isMaster, false);
if (count) {
if (!classExists) {
return 0;
@@ -1430,14 +1429,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;
@@ -1740,9 +1742,11 @@ 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;
}
module.exports = DatabaseController;
// Expose validateQuery for tests
module.exports._validateQuery = validateQuery;
module.exports.filterSensitiveData = filterSensitiveData;
+3
View File
@@ -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));
}
}
+67 -21
View File
@@ -17,9 +17,10 @@ import {
maybeRunAfterEventTrigger,
} 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;
@@ -171,7 +172,7 @@ class ParseLiveQueryServer {
};
return maybeRunAfterEventTrigger('afterEvent', className, res);
})
.then(() => {
.then(async () => {
if (!res.sendEvent) {
return;
}
@@ -179,14 +180,14 @@ class ParseLiveQueryServer {
deletedParseObject = res.object.toJSON();
deletedParseObject.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(error => {
@@ -310,7 +311,7 @@ class ParseLiveQueryServer {
return maybeRunAfterEventTrigger('afterEvent', className, res);
})
.then(
() => {
async () => {
if (!res.sendEvent) {
return;
}
@@ -323,16 +324,14 @@ class ParseLiveQueryServer {
originalParseObject = res.original.toJSON();
originalParseObject.className = 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' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
if (client[functionName]) {
@@ -532,6 +531,53 @@ 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
);
};
res.object = filter(res.object);
res.original = filter(res.original);
}
_getCLPOperation(query: any) {
return typeof query === 'object' &&
Object.keys(query).length == 1 &&
+27
View File
@@ -187,6 +187,9 @@ RestQuery.prototype.execute = function (executeOptions) {
.then(() => {
return this.buildRestWhere();
})
.then(() => {
return this.denyProtectedFields();
})
.then(() => {
return this.handleIncludeAll();
})
@@ -664,6 +667,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) {
+14
View File
@@ -985,6 +985,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) {
+33 -4
View File
@@ -7,6 +7,7 @@ import mime from 'mime';
import logger from '../logger';
const triggers = require('../triggers');
const http = require('http');
const Utils = require('../Utils');
const downloadFileFromURI = uri => {
return new Promise((res, rej) => {
@@ -66,6 +67,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);
@@ -112,6 +119,23 @@ export class FilesRouter {
const base64 = req.body.toString('base64');
const file = new Parse.File(filename, { base64 }, contentType);
const { metadata = {}, tags = {} } = req.fileData || {};
if (req.config && req.config.requestKeywordDenylist) {
// Scan request data for denied keywords
for (const keyword of req.config.requestKeywordDenylist) {
const match =
Utils.objectContainsKeyValue(metadata, keyword.key, keyword.value) ||
Utils.objectContainsKeyValue(tags, keyword.key, keyword.value);
if (match) {
next(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
`Prohibited keyword in request data: ${JSON.stringify(keyword)}.`
)
);
return;
}
}
}
file.setTags(tags);
file.setMetadata(metadata);
const fileSize = Buffer.byteLength(req.body);
@@ -222,10 +246,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);
@@ -237,5 +261,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'
);
}