diff --git a/src/matches-where.test.ts b/src/matches-where.test.ts index 26a2be4a9..8f8fca125 100644 --- a/src/matches-where.test.ts +++ b/src/matches-where.test.ts @@ -53,6 +53,11 @@ await test('matchesWhere', async (t) => { [{ c: { endsWith: 'z' } }, false], [{ a: { endsWith: '1' } }, false], [{ c: { endsWith: 1 } }, false], + // Regression tests for issue #1731: nested _where fail-open + // When intermediate field is not an object, nested predicates should fail (return false) + [{ a: { nested: { eq: 'zzz' } } }, false], // a is a number, not an object + [{ c: { nested: { eq: 'zzz' } } }, false], // c is a string, not an object + [{ or: [{ a: { nested: { eq: 'zzz' } } }] }, false], ] for (const [query, expected] of cases) { diff --git a/src/matches-where.ts b/src/matches-where.ts index 036b00c69..ee3b8a60f 100644 --- a/src/matches-where.ts +++ b/src/matches-where.ts @@ -21,6 +21,19 @@ function getKnownOperators(value: unknown): WhereOperator[] { return ops } +function hasKnownOperatorsAtAnyLevel(value: unknown): boolean { + if (!isJSONObject(value)) return false + + const knownOps = getKnownOperators(value) + if (knownOps.length > 0) return true + + for (const v of Object.values(value)) { + if (hasKnownOperatorsAtAnyLevel(v)) return true + } + + return false +} + export function matchesWhere(obj: JsonObject, where: JsonObject): boolean { for (const [key, value] of Object.entries(where)) { if (key === 'or') { @@ -74,6 +87,8 @@ export function matchesWhere(obj: JsonObject, where: JsonObject): boolean { if (isJSONObject(field)) { if (!matchesWhere(field, value)) return false + } else if (hasKnownOperatorsAtAnyLevel(value)) { + return false } continue