mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6dc487963 | ||
|
|
ecf0814499 | ||
|
|
7aac70cca6 | ||
|
|
6d0b2f5346 | ||
|
|
83fd16c1b9 | ||
|
|
e39d51bd32 | ||
|
|
e42be5c526 | ||
|
|
309f64ced8 | ||
|
|
eb2952fff7 | ||
|
|
5be375dec2 | ||
|
|
ed0baa87af | ||
|
|
ba2b0a9cb9 | ||
|
|
a8aef820af | ||
|
|
af4a0417a9 |
@@ -1,3 +1,52 @@
|
||||
## [5.2.7](https://github.com/parse-community/parse-server/compare/5.2.6...5.2.7) (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)) ([#8185](https://github.com/parse-community/parse-server/issues/8185)) ([ecf0814](https://github.com/parse-community/parse-server/commit/ecf0814499bde31ab6082b6e42854aa65ad2e03e))
|
||||
|
||||
## [5.2.6](https://github.com/parse-community/parse-server/compare/5.2.5...5.2.6) (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)) ([#8182](https://github.com/parse-community/parse-server/issues/8182)) ([6d0b2f5](https://github.com/parse-community/parse-server/commit/6d0b2f534603301bb630d9c8e497af3bc7ff1d09))
|
||||
|
||||
## [5.2.5](https://github.com/parse-community/parse-server/compare/5.2.4...5.2.5) (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) ([#8144](https://github.com/parse-community/parse-server/issues/8144)) ([e39d51b](https://github.com/parse-community/parse-server/commit/e39d51bd329cd978589983bd659db46e1d45aad4))
|
||||
|
||||
## [5.2.4](https://github.com/parse-community/parse-server/compare/5.2.3...5.2.4) (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)) (https://github.com/parse-community/parse-server/pull/8074) ([#8073](https://github.com/parse-community/parse-server/issues/8073)) ([309f64c](https://github.com/parse-community/parse-server/commit/309f64ced8700321df056fb3cc97f15007a00df1))
|
||||
|
||||
## [5.2.3](https://github.com/parse-community/parse-server/compare/5.2.2...5.2.3) (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)) ([#8060](https://github.com/parse-community/parse-server/issues/8060)) ([5be375d](https://github.com/parse-community/parse-server/commit/5be375dec2fa35425c1003ae81c55995ac72af92))
|
||||
|
||||
## [5.2.2](https://github.com/parse-community/parse-server/compare/5.2.1...5.2.2) (2022-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* certificate in Apple Game Center auth adapter not validated; this fixes a security vulnerability in which authentication could be bypassed using a fake certificate; if you are using the Apple Gamer Center auth adapter it is your responsibility to keep its root certificate up-to-date and we advice you read the security advisory ([GHSA-rh9j-f5f8-rvgc](https://github.com/parse-community/parse-server/security/advisories/GHSA-rh9j-f5f8-rvgc)) ([ba2b0a9](https://github.com/parse-community/parse-server/commit/ba2b0a9cb9a568817a114b132a4c2e0911d76df1))
|
||||
|
||||
## [5.2.1](https://github.com/parse-community/parse-server/compare/5.2.0...5.2.1) (2022-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* authentication bypass and denial of service (DoS) vulnerabilities in Apple Game Center auth adapter (GHSA-qf8x-vqjv-92gr) ([#7962](https://github.com/parse-community/parse-server/issues/7962)) ([af4a041](https://github.com/parse-community/parse-server/commit/af4a0417a9f3c1e99b3793806b4b18e04d9fa999))
|
||||
|
||||
# [5.2.0](https://github.com/parse-community/parse-server/compare/5.1.1...5.2.0) (2022-03-24)
|
||||
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.7",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
+5
-6
@@ -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}` : "" %>']
|
||||
}],
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -1652,8 +1675,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,68 +1720,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');
|
||||
}
|
||||
});
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
try {
|
||||
await gcenter.validateAuthData(authData);
|
||||
fail();
|
||||
} catch (e) {
|
||||
expect(e.message).toBe('Apple Game Center - invalid publicKeyUrl: invalid.com');
|
||||
}
|
||||
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 authData = {
|
||||
id: 'G:1965586982',
|
||||
publicKeyUrl: 'http://static.gc.apple.com/public-key/gc-prod-4.cer',
|
||||
timestamp: 1565257031287,
|
||||
signature: '1234',
|
||||
salt: 'DzqqrQ==',
|
||||
bundleId: 'cloud.xtralife.gamecenterauth',
|
||||
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: http://static.gc.apple.com/public-key/gc-prod-4.cer'
|
||||
);
|
||||
}
|
||||
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.'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -654,6 +654,44 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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' });
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('Gridstore Range tests', () => {
|
||||
it('supports range requests', done => {
|
||||
const headers = {
|
||||
|
||||
@@ -1066,6 +1066,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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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-----
|
||||
@@ -32,22 +32,23 @@ function validateGraphToken(authData, options) {
|
||||
});
|
||||
}
|
||||
|
||||
function validateGraphAppId(appIds, authData, options) {
|
||||
async function validateGraphAppId(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.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getFacebookKeyByKeyId = async (keyId, cacheMaxEntries, cacheMaxAge) => {
|
||||
|
||||
@@ -14,20 +14,14 @@ 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) {
|
||||
try {
|
||||
const parsedUrl = new URL(publicKeyUrl);
|
||||
if (parsedUrl.protocol !== 'https:') {
|
||||
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';
|
||||
const regex = /^https:\/\/(?:[-_A-Za-z0-9]+\.){0,}apple\.com\/.*\.cer$/;
|
||||
return regex.test(publicKeyUrl);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
@@ -43,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,
|
||||
@@ -53,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);
|
||||
@@ -103,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) {
|
||||
@@ -114,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,
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -55,31 +55,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.');
|
||||
}
|
||||
@@ -87,7 +83,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.');
|
||||
}
|
||||
@@ -95,7 +91,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,
|
||||
@@ -115,7 +111,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}`);
|
||||
}
|
||||
});
|
||||
@@ -127,7 +127,7 @@ const filterSensitiveData = (
|
||||
aclGroup: any[],
|
||||
auth: any,
|
||||
operation: any,
|
||||
schema: SchemaController.SchemaController,
|
||||
schema: SchemaController.SchemaController | any,
|
||||
className: string,
|
||||
protectedFields: null | Array<any>,
|
||||
object: any
|
||||
@@ -136,7 +136,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;
|
||||
|
||||
@@ -207,27 +208,24 @@ 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;
|
||||
@@ -514,7 +512,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, true);
|
||||
return schemaController
|
||||
.getOneSchema(className, true)
|
||||
.catch(error => {
|
||||
@@ -760,7 +758,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
return schemaController
|
||||
.getOneSchema(className)
|
||||
.catch(error => {
|
||||
@@ -1252,7 +1250,7 @@ class DatabaseController {
|
||||
query = addReadACL(query, aclGroup);
|
||||
}
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
if (count) {
|
||||
if (!classExists) {
|
||||
return 0;
|
||||
@@ -1533,14 +1531,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;
|
||||
@@ -1805,9 +1806,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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,18 @@ import { ParsePubSub } from './ParsePubSub';
|
||||
import SchemaController from '../Controllers/SchemaController';
|
||||
import _ from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { runLiveQueryEventHandlers, getTrigger, runTrigger, resolveError, toJSONwithObjects } from '../triggers';
|
||||
import {
|
||||
runLiveQueryEventHandlers,
|
||||
getTrigger,
|
||||
runTrigger,
|
||||
resolveError,
|
||||
toJSONwithObjects,
|
||||
} 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;
|
||||
@@ -185,14 +192,14 @@ class ParseLiveQueryServer {
|
||||
if (res.object && typeof res.object.toJSON === 'function') {
|
||||
deletedParseObject = toJSONwithObjects(res.object, res.object.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 (e) {
|
||||
const error = resolveError(e);
|
||||
@@ -339,16 +346,14 @@ class ParseLiveQueryServer {
|
||||
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' + res.event.charAt(0).toUpperCase() + res.event.slice(1);
|
||||
if (client[functionName]) {
|
||||
client[functionName](requestId, currentParseObject, originalParseObject);
|
||||
@@ -540,6 +545,54 @@ 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,
|
||||
query
|
||||
);
|
||||
};
|
||||
res.object = filter(res.object);
|
||||
res.original = filter(res.original);
|
||||
}
|
||||
|
||||
_getCLPOperation(query: any) {
|
||||
return typeof query === 'object' &&
|
||||
Object.keys(query).length == 1 &&
|
||||
|
||||
@@ -202,6 +202,9 @@ RestQuery.prototype.execute = function (executeOptions) {
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
})
|
||||
.then(() => {
|
||||
return this.denyProtectedFields();
|
||||
})
|
||||
.then(() => {
|
||||
return this.handleIncludeAll();
|
||||
})
|
||||
@@ -688,6 +691,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) {
|
||||
|
||||
@@ -1015,6 +1015,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) {
|
||||
|
||||
@@ -66,6 +66,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);
|
||||
@@ -250,10 +256,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);
|
||||
|
||||
Reference in New Issue
Block a user