From 5ce8ea9c97366c14f8761283e0f6c46b17f5ce56 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 8 Mar 2026 05:39:54 +0000 Subject: [PATCH 1/4] fix --- spec/ProtectedFields.spec.js | 102 +++++++++++++++++++++++++++++++++++ src/RestQuery.js | 24 ++++++--- 2 files changed, 118 insertions(+), 8 deletions(-) diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js index 8195985dcb..e39ce6cfe1 100644 --- a/spec/ProtectedFields.spec.js +++ b/spec/ProtectedFields.spec.js @@ -1700,4 +1700,106 @@ 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); + }); + }); }); diff --git a/src/RestQuery.js b/src/RestQuery.js index 4c64801ee7..4216c5da37 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -847,15 +847,23 @@ _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) => { + for (const key of protectedFields) { + if (where[key]) { + 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 From 0a13b3fc31d89b367062c5adbaa441c2191448cf Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:33:45 +0000 Subject: [PATCH 2/4] fix https://github.com/parse-community/parse-server/pull/10140#discussion_r2901906550 --- spec/ProtectedFields.spec.js | 20 ++++++++++++++++++++ src/RestQuery.js | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js index e39ce6cfe1..410ba3e895 100644 --- a/spec/ProtectedFields.spec.js +++ b/spec/ProtectedFields.spec.js @@ -1801,5 +1801,25 @@ describe('ProtectedFields', function () { 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, + }) + ); + }); }); }); diff --git a/src/RestQuery.js b/src/RestQuery.js index 4216c5da37..1d071160fa 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -849,7 +849,7 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { ) || []; const checkWhere = (where) => { for (const key of protectedFields) { - if (where[key]) { + if (key in where) { throw createSanitizedError( Parse.Error.OPERATION_FORBIDDEN, `This user is not allowed to query ${key} on class ${this.className}`, From ae9a83634d96445b84a1bb31c571dcbcb3e7cb63 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:01:42 +0000 Subject: [PATCH 3/4] fix --- spec/ProtectedFields.spec.js | 8 ++++++++ src/RestQuery.js | 3 +++ 2 files changed, 11 insertions(+) diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js index 410ba3e895..aec5e77688 100644 --- a/spec/ProtectedFields.spec.js +++ b/spec/ProtectedFields.spec.js @@ -1821,5 +1821,13 @@ describe('ProtectedFields', function () { }) ); }); + + 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(); + }); }); }); diff --git a/src/RestQuery.js b/src/RestQuery.js index 1d071160fa..c335b1e633 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -848,6 +848,9 @@ _UnsafeRestQuery.prototype.denyProtectedFields = async function () { this.findOptions ) || []; const checkWhere = (where) => { + if (typeof where !== 'object' || where === null) { + return; + } for (const key of protectedFields) { if (key in where) { throw createSanitizedError( From e3458b6a29216f295666caa94ed3e2b7c9726248 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:16:35 +0000 Subject: [PATCH 4/4] Update ProtectedFields.spec.js --- spec/ProtectedFields.spec.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/spec/ProtectedFields.spec.js b/spec/ProtectedFields.spec.js index aec5e77688..ba5eb96e2f 100644 --- a/spec/ProtectedFields.spec.js +++ b/spec/ProtectedFields.spec.js @@ -1822,12 +1822,19 @@ describe('ProtectedFields', function () { ); }); - 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(); + 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(); }); }); });