Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
130 changes: 130 additions & 0 deletions spec/ProtectedFields.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1700,4 +1700,134 @@ describe('ProtectedFields', function () {
done();
});
});

describe('query on protected fields via logical operators', function () {
let user;
let otherUser;
const testEmail = 'victim@example.com';
const otherEmail = 'other@example.com';

beforeEach(async function () {
await reconfigureServer({
protectedFields: {
_User: { '*': ['email'] },
},
});
user = new Parse.User();
user.setUsername('victim' + Date.now());
user.setPassword('password');
user.setEmail(testEmail);
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
user.setACL(acl);
await user.save(null, { useMasterKey: true });

otherUser = new Parse.User();
otherUser.setUsername('attacker' + Date.now());
otherUser.setPassword('password');
otherUser.setEmail(otherEmail);
const acl2 = new Parse.ACL();
acl2.setPublicReadAccess(true);
otherUser.setACL(acl2);
await otherUser.save(null, { useMasterKey: true });
await Parse.User.logIn(otherUser.getUsername(), 'password');
});

it('should deny query on protected field via $or', async function () {
const q1 = new Parse.Query(Parse.User);
q1.equalTo('email', testEmail);
const query = Parse.Query.or(q1);
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should deny query on protected field via $and', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { $and: [{ email: testEmail }] } });
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should deny query on protected field via $nor', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { $nor: [{ email: testEmail }] } });
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should deny query on protected field via nested $or inside $and', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { $and: [{ $or: [{ email: testEmail }] }] } });
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should deny query on protected field via $or with $regex', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { $or: [{ email: { $regex: '^victim' } }] } });
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should allow $or query on non-protected fields', async function () {
const q1 = new Parse.Query(Parse.User);
q1.equalTo('username', user.getUsername());
const query = Parse.Query.or(q1);
const results = await query.find();
expect(results.length).toBe(1);
expect(results[0].id).toBe(user.id);
});

it('should allow master key to query on protected fields via $or', async function () {
const q1 = new Parse.Query(Parse.User);
q1.equalTo('email', testEmail);
const query = Parse.Query.or(q1);
const results = await query.find({ useMasterKey: true });
expect(results.length).toBe(1);
expect(results[0].id).toBe(user.id);
});

it('should deny query on protected field with falsy value', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { email: null } });
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should deny query on protected field with falsy value via $or', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { $or: [{ email: null }] } });
await expectAsync(query.find()).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});

it('should handle malformed $or with null element', async function () {
const query = new Parse.Query(Parse.User);
query.withJSON({ where: { $or: [null, { username: 'test' }] } });
// Should not throw TypeError from denyProtectedFields;
// may fail downstream in validateQuery (pre-existing issue)
await expectAsync(query.find()).toBeRejected();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
});
});
27 changes: 19 additions & 8 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -847,15 +847,26 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () {
this.auth,
this.findOptions
) || [];
for (const key of protectedFields) {
if (this.restWhere[key]) {
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`This user is not allowed to query ${key} on class ${this.className}`,
this.config
);
const checkWhere = (where) => {
if (typeof where !== 'object' || where === null) {
return;
}
}
for (const key of protectedFields) {
if (key in where) {
throw createSanitizedError(
Parse.Error.OPERATION_FORBIDDEN,
`This user is not allowed to query ${key} on class ${this.className}`,
this.config
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
for (const op of ['$or', '$and', '$nor']) {
if (Array.isArray(where[op])) {
where[op].forEach(subQuery => checkWhere(subQuery));
}
}
};
checkWhere(this.restWhere);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

// Augments this.response with all pointers on an object
Expand Down
Loading