mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
fix: Protected fields bypass via logical query operators ([GHSA-72hp-qff8-4pvv](https://github.com/parse-community/parse-server/security/advisories/GHSA-72hp-qff8-4pvv)) (#10139)
This commit is contained in:
@@ -1700,4 +1700,141 @@ 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 not throw TypeError in denyProtectedFields for null element in $or', async function () {
|
||||
const Config = require('../lib/Config');
|
||||
const authModule = require('../lib/Auth');
|
||||
const RestQuery = require('../lib/RestQuery');
|
||||
const config = Config.get(Parse.applicationId);
|
||||
const restQuery = await RestQuery({
|
||||
method: RestQuery.Method.find,
|
||||
config,
|
||||
auth: authModule.nobody(config),
|
||||
className: '_User',
|
||||
restWhere: { $or: [null, { username: 'test' }] },
|
||||
});
|
||||
await expectAsync(restQuery.denyProtectedFields()).toBeResolved();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+19
-8
@@ -827,15 +827,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
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const op of ['$or', '$and', '$nor']) {
|
||||
if (Array.isArray(where[op])) {
|
||||
where[op].forEach(subQuery => checkWhere(subQuery));
|
||||
}
|
||||
}
|
||||
};
|
||||
checkWhere(this.restWhere);
|
||||
};
|
||||
|
||||
// Augments this.response with all pointers on an object
|
||||
|
||||
Reference in New Issue
Block a user