From a538090827fb6adbc44b0a3a056ab4ae6c100f42 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 6 Aug 2026 10:56:36 -0400 Subject: [PATCH 1/2] fix: validate logical query structures --- .../queryValidation/validateQueryPaths.ts | 4 +- .../drafts/appendVersionToQueryKey.spec.ts | 30 ++++++++++++++ .../drafts/appendVersionToQueryKey.ts | 2 +- test/access-control/int.spec.ts | 41 +++++++++++++------ 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 packages/payload/src/versions/drafts/appendVersionToQueryKey.spec.ts diff --git a/packages/payload/src/database/queryValidation/validateQueryPaths.ts b/packages/payload/src/database/queryValidation/validateQueryPaths.ts index 389f53ebae9..5816706c330 100644 --- a/packages/payload/src/database/queryValidation/validateQueryPaths.ts +++ b/packages/payload/src/database/queryValidation/validateQueryPaths.ts @@ -50,7 +50,7 @@ export async function validateQueryPaths({ for (const path in where) { const constraint = where[path] - if ((path === 'and' || path === 'or') && Array.isArray(constraint)) { + if (['and', 'or'].includes(path.toLowerCase()) && Array.isArray(constraint)) { for (const item of constraint) { if (collectionConfig) { promises.push( @@ -80,6 +80,8 @@ export async function validateQueryPaths({ ) } } + } else if (Array.isArray(constraint)) { + errors.push({ path }) } else if (!Array.isArray(constraint)) { for (const operator in constraint) { const val = constraint[operator as keyof typeof constraint] diff --git a/packages/payload/src/versions/drafts/appendVersionToQueryKey.spec.ts b/packages/payload/src/versions/drafts/appendVersionToQueryKey.spec.ts new file mode 100644 index 00000000000..b12edb9c9e6 --- /dev/null +++ b/packages/payload/src/versions/drafts/appendVersionToQueryKey.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' + +import { appendVersionToQueryKey } from './appendVersionToQueryKey.js' + +describe('appendVersionToQueryKey', () => { + it.each(['aNd', 'oR'])( + 'should preserve case-insensitive %s conditions when prefixing version fields', + (logicalOperator) => { + expect( + appendVersionToQueryKey({ + [logicalOperator]: [ + { + title: { + equals: 'example', + }, + }, + ], + }), + ).toStrictEqual({ + [logicalOperator.toLowerCase()]: [ + { + 'version.title': { + equals: 'example', + }, + }, + ], + }) + }, + ) +}) diff --git a/packages/payload/src/versions/drafts/appendVersionToQueryKey.ts b/packages/payload/src/versions/drafts/appendVersionToQueryKey.ts index f42070ed05f..703bf63e751 100644 --- a/packages/payload/src/versions/drafts/appendVersionToQueryKey.ts +++ b/packages/payload/src/versions/drafts/appendVersionToQueryKey.ts @@ -2,7 +2,7 @@ import type { Where } from '../../types/index.js' export const appendVersionToQueryKey = (query: Where = {}): Where => { return Object.entries(query).reduce((res, [key, val]) => { - if (['AND', 'and', 'OR', 'or'].includes(key) && Array.isArray(val)) { + if (['and', 'or'].includes(key.toLowerCase()) && Array.isArray(val)) { return { ...res, [key.toLowerCase()]: val.map((subQuery) => appendVersionToQueryKey(subQuery)), diff --git a/test/access-control/int.spec.ts b/test/access-control/int.spec.ts index d21619aa447..40283288853 100644 --- a/test/access-control/int.spec.ts +++ b/test/access-control/int.spec.ts @@ -266,25 +266,40 @@ describe('Access Control', () => { expect(retrievedDoc.restrictedField).toBeUndefined() }) - it('should error when querying field without read access', async () => { - const { id } = await createDoc({ restrictedField: 'restricted' }) + it.each(['AND', 'OR', 'AnD', 'oR'])( + 'should validate field read access inside case-insensitive %s conditions', + async (logicalOperator) => { + const { id } = await createDoc({ restrictedField: 'restricted' }) + + await expect( + payload.find({ + collection: slug, + overrideAccess: false, + where: { + [logicalOperator]: [ + { + id: { equals: id }, + }, + { + restrictedField: { + equals: 'restricted', + }, + }, + ], + }, + }), + ).rejects.toThrow('The following path cannot be queried: restrictedField') + }, + ) + it('should reject array-valued field conditions', async () => { await expect( payload.find({ collection: slug, overrideAccess: false, where: { - and: [ - { - id: { equals: id }, - }, - { - restrictedField: { - equals: 'restricted', - }, - }, - ], - }, + restrictedField: [{ equals: 'restricted' }], + } as any, }), ).rejects.toThrow('The following path cannot be queried: restrictedField') }) From bb7a07dbd115bff0efdaa0cd00aa61139023a787 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 6 Aug 2026 22:28:42 -0400 Subject: [PATCH 2/2] chore: retrigger checks