Skip to content

Facebook signup error206 #8737

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,8 @@
"space-infix-ops": "error",
"no-useless-escape": "off",
"require-atomic-updates": "off"
},
"globals": {
"Parse": true
}
}
14 changes: 14 additions & 0 deletions changelogs/CHANGELOG_release.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
## [6.2.2](https://github.com/parse-community/parse-server/compare/6.2.1...6.2.2) (2023-09-04)


### Bug Fixes

* Parse Pointer allows to access internal Parse Server classes and circumvent `beforeFind` query trigger; fixes security vulnerability [GHSA-fcv6-fg5r-jm9q](https://github.com/parse-community/parse-server/security/advisories/GHSA-fcv6-fg5r-jm9q) ([be4c7e2](https://github.com/parse-community/parse-server/commit/be4c7e23c63a2fb690685665cebed0de26be05c5))

## [6.2.1](https://github.com/parse-community/parse-server/compare/6.2.0...6.2.1) (2023-06-28)


### Bug Fixes

* Remote code execution via MongoDB BSON parser through prototype pollution; fixes security vulnerability [GHSA-462x-c3jw-7vr6](https://github.com/parse-community/parse-server/security/advisories/GHSA-462x-c3jw-7vr6) ([#8674](https://github.com/parse-community/parse-server/issues/8674)) ([3dd99dd](https://github.com/parse-community/parse-server/commit/3dd99dd80e27e5e1d99b42844180546d90c7aa90))

# [6.2.0](https://github.com/parse-community/parse-server/compare/6.1.0...6.2.0) (2023-05-20)


Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "6.2.0",
"version": "6.2.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {
Expand Down
29 changes: 29 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2381,6 +2381,35 @@ describe('beforeFind hooks', () => {
})
.then(() => done());
});

it('should run beforeFind on pointers and array of pointers from an object', async () => {
const obj1 = new Parse.Object('TestObject');
const obj2 = new Parse.Object('TestObject2');
const obj3 = new Parse.Object('TestObject');
obj2.set('aField', 'aFieldValue');
await obj2.save();
obj1.set('pointerField', obj2);
obj3.set('pointerFieldArray', [obj2]);
await obj1.save();
await obj3.save();
const spy = jasmine.createSpy('beforeFindSpy');
Parse.Cloud.beforeFind('TestObject2', spy);
const query = new Parse.Query('TestObject');
await query.get(obj1.id);
// Pointer not included in query so we don't expect beforeFind to be called
expect(spy).not.toHaveBeenCalled();
const query2 = new Parse.Query('TestObject');
query2.include('pointerField');
const res = await query2.get(obj1.id);
expect(res.get('pointerField').get('aField')).toBe('aFieldValue');
// Pointer included in query so we expect beforeFind to be called
expect(spy).toHaveBeenCalledTimes(1);
const query3 = new Parse.Query('TestObject');
query3.include('pointerFieldArray');
const res2 = await query3.get(obj3.id);
expect(res2.get('pointerFieldArray')[0].get('aField')).toBe('aFieldValue');
expect(spy).toHaveBeenCalledTimes(2);
});
});

describe('afterFind hooks', () => {
Expand Down
1 change: 0 additions & 1 deletion spec/ParseGraphQLServer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5275,7 +5275,6 @@ describe('ParseGraphQLServer', () => {

it('should only count', async () => {
await prepareData();

await parseGraphQLServer.parseGraphQLSchema.schemaCache.clear();

const where = {
Expand Down
2 changes: 1 addition & 1 deletion spec/ParseRole.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ describe('Parse Role testing', () => {
return Promise.all(promises);
};

const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough();
const restExecute = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();

let user, auth, getAllRolesSpy;
createTestUser()
Expand Down
44 changes: 24 additions & 20 deletions spec/RestQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,15 +399,16 @@ describe('RestQuery.each', () => {
}
const config = Config.get('test');
await Parse.Object.saveAll(objects);
const query = new RestQuery(
const query = await RestQuery({
method: RestQuery.Method.find,
config,
auth.master(config),
'Object',
{ value: { $gt: 2 } },
{ limit: 2 }
);
auth: auth.master(config),
className: 'Object',
restWhere: { value: { $gt: 2 } },
restOptions: { limit: 2 },
});
const spy = spyOn(query, 'execute').and.callThrough();
const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
const results = [];
await query.each(result => {
expect(result.value).toBeGreaterThan(2);
Expand Down Expand Up @@ -438,34 +439,37 @@ describe('RestQuery.each', () => {
* Two queries needed since objectId are sorted and we can't know which one
* going to be the first and then skip by the $gt added by each
*/
const queryOne = new RestQuery(
const queryOne = await RestQuery({
method: RestQuery.Method.get,
config,
auth.master(config),
'Letter',
{
auth: auth.master(config),
className: 'Letter',
restWhere: {
numbers: {
__type: 'Pointer',
className: 'Number',
objectId: object1.id,
},
},
{ limit: 1 }
);
const queryTwo = new RestQuery(
restOptions: { limit: 1 },
});

const queryTwo = await RestQuery({
method: RestQuery.Method.get,
config,
auth.master(config),
'Letter',
{
auth: auth.master(config),
className: 'Letter',
restWhere: {
numbers: {
__type: 'Pointer',
className: 'Number',
objectId: object2.id,
},
},
{ limit: 1 }
);
restOptions: { limit: 1 },
});

const classSpy = spyOn(RestQuery.prototype, 'execute').and.callThrough();
const classSpy = spyOn(RestQuery._UnsafeRestQuery.prototype, 'execute').and.callThrough();
const resultsOne = [];
const resultsTwo = [];
await queryOne.each(result => {
Expand Down
72 changes: 72 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,46 @@ describe('rest create', () => {
});
});

it('test facebook signup, login', async () => {
const data = {
authData: {
facebook: {
id: '8675309',
access_token: 'jenny',
},
},
};

let newUserSignedUpByFacebookObjectId;

try {
const firstResponse = await rest.create(config, auth.nobody(config), '_User', data);

expect(typeof firstResponse.response.objectId).toEqual('string');
expect(typeof firstResponse.response.createdAt).toEqual('string');
expect(typeof firstResponse.response.sessionToken).toEqual('string');
newUserSignedUpByFacebookObjectId = firstResponse.response.objectId;

const secondResponse = await rest.create(config, auth.nobody(config), '_User', data);

expect(typeof secondResponse.response.objectId).toEqual('string');
expect(typeof secondResponse.response.createdAt).toEqual('string');
expect(typeof secondResponse.response.username).toEqual('string');
expect(typeof secondResponse.response.updatedAt).toEqual('string');
expect(secondResponse.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);

const sessionResponse = await rest.find(config, auth.master(config), '_Session', {
sessionToken: secondResponse.response.sessionToken,
});

expect(sessionResponse.results.length).toEqual(1);
const output = sessionResponse.results[0];
expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
} catch (err) {
jfail(err);
}
});

it('stores pointers', done => {
const obj = {
foo: 'bar',
Expand Down Expand Up @@ -660,6 +700,38 @@ describe('rest create', () => {
});
});

it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
const obj2 = new Parse.Object('TestObject');
// Anyone is can basically create a pointer to any object
// or some developers can use master key in some hook to link
// private objects to standard objects
obj2.set('pointer', masterKeyOnlyClassObject);
await obj2.save();
const query = new Parse.Query('TestObject');
query.include('pointer');
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
"Clients aren't allowed to perform the get operation on the _PushStatus collection."
);
});

it('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
const obj2 = new Parse.Object('TestObject');
obj2.set('globalConfigPointer', {
__type: 'Pointer',
className: '_GlobalConfig',
objectId: 1,
});
await obj2.save();
const query = new Parse.Query('TestObject');
query.include('globalConfigPointer');
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
"Clients aren't allowed to perform the get operation on the _GlobalConfig collection."
);
});

it('locks down session', done => {
let currentUser;
Parse.User.signUp('foo', 'bar')
Expand Down
65 changes: 65 additions & 0 deletions spec/vulnerabilities.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,71 @@ describe('Vulnerabilities', () => {
);
});

it('denies creating global config with polluted data', async () => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
};
const params = {
method: 'PUT',
url: 'http://localhost:8378/1/config',
json: true,
body: {
params: {
welcomeMesssage: 'Welcome to Parse',
foo: { _bsontype: 'Code', code: 'shell' },
},
},
headers,
};
const response = await request(params).catch(e => e);
expect(response.status).toBe(400);
const text = JSON.parse(response.text);
expect(text.code).toBe(Parse.Error.INVALID_KEY_NAME);
expect(text.error).toBe(
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
);
});

it('denies direct database write wih prohibited keys', async () => {
const Config = require('../lib/Config');
const config = Config.get(Parse.applicationId);
const user = {
objectId: '1234567890',
username: 'hello',
password: 'pass',
_session_token: 'abc',
foo: { _bsontype: 'Code', code: 'shell' },
};
await expectAsync(config.database.create('_User', user)).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
)
);
});

it('denies direct database update wih prohibited keys', async () => {
const Config = require('../lib/Config');
const config = Config.get(Parse.applicationId);
const user = {
objectId: '1234567890',
username: 'hello',
password: 'pass',
_session_token: 'abc',
foo: { _bsontype: 'Code', code: 'shell' },
};
await expectAsync(
config.database.update('_User', { _id: user.objectId }, user)
).toBeRejectedWith(
new Parse.Error(
Parse.Error.INVALID_KEY_NAME,
'Prohibited keyword in request data: {"key":"_bsontype","value":"Code"}.'
)
);
});

it('denies creating a hook with polluted data', async () => {
const express = require('express');
const bodyParser = require('body-parser');
Expand Down
Loading