mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8011b2fdac | ||
|
|
066f29673a | ||
|
|
e6dc487963 | ||
|
|
ecf0814499 | ||
|
|
7aac70cca6 | ||
|
|
6d0b2f5346 | ||
|
|
83fd16c1b9 | ||
|
|
e39d51bd32 |
@@ -1,3 +1,31 @@
|
||||
## [5.2.8](https://github.com/parse-community/parse-server/compare/5.2.7...5.2.8) (2022-10-14)
|
||||
|
||||
|
||||
### 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)) ([#8235](https://github.com/parse-community/parse-server/issues/8235)) ([066f296](https://github.com/parse-community/parse-server/commit/066f29673ab4030b6b5b90c0c0326f7d3fe7612a))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.2.4",
|
||||
"version": "5.2.8",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "5.2.4",
|
||||
"version": "5.2.8",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
+199
-10
@@ -692,7 +692,198 @@ describe('Parse.File testing', () => {
|
||||
});
|
||||
});
|
||||
|
||||
xdescribe('Gridstore Range tests', () => {
|
||||
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',
|
||||
@@ -781,7 +972,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',
|
||||
@@ -879,8 +1070,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',
|
||||
@@ -888,12 +1079,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.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
@@ -208,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;
|
||||
@@ -515,7 +512,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, true);
|
||||
return schemaController
|
||||
.getOneSchema(className, true)
|
||||
.catch(error => {
|
||||
@@ -761,7 +758,7 @@ class DatabaseController {
|
||||
if (acl) {
|
||||
query = addWriteACL(query, acl);
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
return schemaController
|
||||
.getOneSchema(className)
|
||||
.catch(error => {
|
||||
@@ -1253,7 +1250,7 @@ class DatabaseController {
|
||||
query = addReadACL(query, aclGroup);
|
||||
}
|
||||
}
|
||||
validateQuery(query);
|
||||
validateQuery(query, isMaster, false);
|
||||
if (count) {
|
||||
if (!classExists) {
|
||||
return 0;
|
||||
@@ -1809,7 +1806,7 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -271,5 +271,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'
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user