Skip to content
Merged
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
101 changes: 101 additions & 0 deletions spec/RestQuery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,104 @@ describe('RestQuery.each', () => {
]);
});
});

describe('redirectClassNameForKey security', () => {
let config;

beforeEach(() => {
config = Config.get('test');
});

it('should scope _Session results to the current user when redirected via redirectClassNameForKey', async () => {
// Create two users with sessions (without logging out, to preserve sessions)
const user1 = await Parse.User.signUp('user1', 'password1');
const sessionToken1 = user1.getSessionToken();

// Sign up user2 via REST to avoid logging out user1
await request({
method: 'POST',
url: Parse.serverURL + '/users',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
},
body: { username: 'user2', password: 'password2' },
});

// Create a public class with a relation field pointing to _Session
// (using masterKey to create the object and relation schema)
const obj = new Parse.Object('PublicData');
const relation = obj.relation('pivot');
// Add a fake pointer to _Session to establish the relation schema
relation.add(Parse.Object.fromJSON({ className: '_Session', objectId: 'fakeId' }));
await obj.save(null, { useMasterKey: true });

// Authenticated user queries with redirectClassNameForKey
const userAuth = await auth.getAuthForSessionToken({
config,
sessionToken: sessionToken1,
});
const result = await rest.find(config, userAuth, 'PublicData', {}, { redirectClassNameForKey: 'pivot' });

// Should only see user1's own session, not user2's
expect(result.results.length).toBe(1);
expect(result.results[0].user.objectId).toBe(user1.id);
});

it('should reject unauthenticated access to _Session via redirectClassNameForKey', async () => {
// Create a user so a session exists
await Parse.User.signUp('victim', 'password123');
await Parse.User.logOut();

// Create a public class with a relation to _Session
const obj = new Parse.Object('PublicData');
const relation = obj.relation('pivot');
relation.add(Parse.Object.fromJSON({ className: '_Session', objectId: 'fakeId' }));
await obj.save(null, { useMasterKey: true });

// Unauthenticated query with redirectClassNameForKey
await expectAsync(
rest.find(config, auth.nobody(config), 'PublicData', {}, { redirectClassNameForKey: 'pivot' })
).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.INVALID_SESSION_TOKEN })
);
});

it('should block redirectClassNameForKey to master-only classes', async () => {
// Create a public class with a relation to _JobStatus (master-only)
const obj = new Parse.Object('PublicData');
const relation = obj.relation('jobPivot');
relation.add(Parse.Object.fromJSON({ className: '_JobStatus', objectId: 'fakeId' }));
await obj.save(null, { useMasterKey: true });

// Create a user for authenticated access
const user = await Parse.User.signUp('attacker', 'password123');
const sessionToken = user.getSessionToken();
const userAuth = await auth.getAuthForSessionToken({ config, sessionToken });

// Authenticated query should be blocked
await expectAsync(
rest.find(config, userAuth, 'PublicData', {}, { redirectClassNameForKey: 'jobPivot' })
).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN })
);
});

it('should allow redirectClassNameForKey between regular classes', async () => {
// Create target class objects
const wheel1 = new Parse.Object('Wheel');
await wheel1.save();

// Create source class with relation to Wheel
const car = new Parse.Object('Car');
const relation = car.relation('wheels');
relation.add(wheel1);
await car.save();

// Query with redirectClassNameForKey should work normally
const result = await rest.find(config, auth.nobody(config), 'Car', {}, { redirectClassNameForKey: 'wheels' });
expect(result.results.length).toBe(1);
expect(result.results[0].objectId).toBe(wheel1.id);
});
});
29 changes: 29 additions & 0 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,35 @@ _UnsafeRestQuery.prototype.redirectClassNameForKey = function () {
.then(newClassName => {
this.className = newClassName;
this.redirectClassName = newClassName;

// Re-apply security checks for the redirected class name, since the
// checks in the constructor and in rest.find ran against the original
// class name before the redirect.
if (!this.auth.isMaster) {
enforceRoleSecurity('find', this.className, this.auth, this.config);

if (this.className === '_Session') {
if (!this.auth.user) {
throw createSanitizedError(
Parse.Error.INVALID_SESSION_TOKEN,
'Invalid session token',
this.config
);
}
this.restWhere = {
$and: [
this.restWhere,
{
user: {
__type: 'Pointer',
className: '_User',
objectId: this.auth.user.id,
},
},
],
};
}
}
});
};

Expand Down
Loading