From 2ae33d4e14498222bda7be20eb8f111ddad2fbcf Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 6 Aug 2026 15:38:17 -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..448198e30a8 --- /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'])( + 'preserves 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 2b152985e4f..1e37d72c525 100644 --- a/test/access-control/int.spec.ts +++ b/test/access-control/int.spec.ts @@ -269,25 +269,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'])( + 'validates field read access inside case-insensitive %s conditions', + async (logicalOperator) => { + const { id } = await createDoc({ restrictedField: 'example' }) + + await expect( + payload.find({ + collection: slug, + overrideAccess: false, + where: { + [logicalOperator]: [ + { + id: { equals: id }, + }, + { + restrictedField: { + equals: 'example', + }, + }, + ], + }, + }), + ).rejects.toThrow('The following path cannot be queried: restrictedField') + }, + ) + it('rejects array-valued field conditions', async () => { await expect( payload.find({ collection: slug, overrideAccess: false, where: { - and: [ - { - id: { equals: id }, - }, - { - restrictedField: { - equals: 'restricted', - }, - }, - ], - }, + restrictedField: [{ equals: 'example' }], + } as any, }), ).rejects.toThrow('The following path cannot be queried: restrictedField') }) From fca097e89a6435965fe0d8932779f835e0c1135b Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Thu, 6 Aug 2026 22:29:07 -0400 Subject: [PATCH 2/2] chore: retrigger checks