Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: prevent security issue where findOneBy with all null/undefined conditions returns first record#11984

Open
pierreeurope wants to merge 2 commits intotypeorm:masterfrom
pierreeurope:fix/findOneBy-null-undefined-safety
Open

fix: prevent security issue where findOneBy with all null/undefined conditions returns first record#11984
pierreeurope wants to merge 2 commits intotypeorm:masterfrom
pierreeurope:fix/findOneBy-null-undefined-safety

Conversation

@pierreeurope
Copy link

Description

Fixes #11873 - Security issue where findOneBy({ id: null }) or findOneBy({ id: undefined }) returns the FIRST record instead of null.

Problem

Even after PR #11332 added the invalidWhereValuesBehavior configuration, the default behavior ("ignore") still creates a security vulnerability:

When ALL conditions in a WHERE clause are null/undefined, they're all ignored, resulting in:

  • No WHERE clause in the SQL query
  • The query returns the first record from the table
  • This can lead to unintended data exposure

Example:

// With default configuration:
const user = await userRepository.findOneBy({ id: null })
// Returns the FIRST user instead of null! 🔴 Security issue

Solution

Added validation in findOneBy(), findOne(), and related methods to detect when all WHERE conditions would be ignored. In this case, the methods now return null immediately without executing a query.

The fix:

  1. Added hasValidWhereConditions() helper that checks if at least one condition is valid
  2. Modified findOneBy() and findOne() to return null when all conditions are invalid
  3. findOneByOrFail() and findOneOrFail() inherit the fix and throw EntityNotFoundError as expected

Behavior

  • When all conditions are null/undefined with default config: Returns null (safe)
  • When at least one condition is valid: Works normally
  • With invalidWhereValuesBehavior.null = 'throw': Still throws (respected)
  • With invalidWhereValuesBehavior.null = 'sql-null': Still converts to SQL NULL (respected)

Tests

Added comprehensive tests covering:

  • findOneBy({ id: null }) returns null
  • findOneBy({ id: undefined }) returns null
  • findOneBy({ id: null, title: null }) returns null (multiple conditions)
  • findOne({ where: { id: null } }) returns null
  • findOneByOrFail({ id: null }) throws EntityNotFoundError
  • ✅ Mixed valid/invalid conditions work normally

All tests pass ✓

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • npm run format to apply prettier formatting
  • This pull request links relevant issues as Fixes #11873
  • There are new or updated unit tests validating the change
  • The new commits follow conventions explained in CONTRIBUTING.md

Breaking Changes

None. This fix makes the default behavior safer without breaking existing valid use cases.

…onditions returns first record

Fixes typeorm#11873

The issue: When calling findOneBy({ id: null }) or findOneBy({ id: undefined }),
the WHERE clause gets dropped entirely (due to default 'ignore' behavior from typeorm#11332),
causing the query to return the first record instead of null. This is a security issue.

Solution: Added validation in findOneBy, findOne, and related methods to check if
all WHERE conditions are null/undefined after applying invalidWhereValuesBehavior
rules. When all conditions are invalid, these methods now return null instead of
querying without a WHERE clause.

Changes:
- Added hasValidWhereConditions() helper method to EntityManager
- Modified findOneBy() to return null when all conditions are null/undefined
- Modified findOne() to return null when all conditions are null/undefined
- findOneByOrFail() and findOneOrFail() inherit the fix and throw EntityNotFoundError
- Added comprehensive tests covering all scenarios

The fix respects existing invalidWhereValuesBehavior configuration:
- If null/undefined behavior is 'throw' or 'sql-null', those are still handled
- Only when behavior is 'ignore' (default) and ALL conditions are ignored, return null

Signed-off-by: pierreeurope <[email protected]>
@qodo-free-for-open-source-projects

User description

Description

Fixes #11873 - Security issue where findOneBy({ id: null }) or findOneBy({ id: undefined }) returns the FIRST record instead of null.

Problem

Even after PR #11332 added the invalidWhereValuesBehavior configuration, the default behavior ("ignore") still creates a security vulnerability:

When ALL conditions in a WHERE clause are null/undefined, they're all ignored, resulting in:

  • No WHERE clause in the SQL query
  • The query returns the first record from the table
  • This can lead to unintended data exposure

Example:

// With default configuration:
const user = await userRepository.findOneBy({ id: null })
// Returns the FIRST user instead of null! 🔴 Security issue

Solution

Added validation in findOneBy(), findOne(), and related methods to detect when all WHERE conditions would be ignored. In this case, the methods now return null immediately without executing a query.

The fix:

  1. Added hasValidWhereConditions() helper that checks if at least one condition is valid
  2. Modified findOneBy() and findOne() to return null when all conditions are invalid
  3. findOneByOrFail() and findOneOrFail() inherit the fix and throw EntityNotFoundError as expected

Behavior

  • When all conditions are null/undefined with default config: Returns null (safe)
  • When at least one condition is valid: Works normally
  • With invalidWhereValuesBehavior.null = 'throw': Still throws (respected)
  • With invalidWhereValuesBehavior.null = 'sql-null': Still converts to SQL NULL (respected)

Tests

Added comprehensive tests covering:

  • findOneBy({ id: null }) returns null
  • findOneBy({ id: undefined }) returns null
  • findOneBy({ id: null, title: null }) returns null (multiple conditions)
  • findOne({ where: { id: null } }) returns null
  • findOneByOrFail({ id: null }) throws EntityNotFoundError
  • ✅ Mixed valid/invalid conditions work normally

All tests pass ✓

Pull-Request Checklist

  • Code is up-to-date with the master branch
  • npm run format to apply prettier formatting
  • This pull request links relevant issues as Fixes #11873
  • There are new or updated unit tests validating the change
  • The new commits follow conventions explained in CONTRIBUTING.md

Breaking Changes

None. This fix makes the default behavior safer without breaking existing valid use cases.


PR Type

Bug fix, Tests


Description

  • Prevent security vulnerability where findOneBy() with all null/undefined conditions returns first record

  • Added hasValidWhereConditions() helper to validate WHERE clause conditions

  • Modified findOneBy() and findOne() to return null when all conditions are invalid

  • Added comprehensive test coverage for null/undefined condition handling scenarios


Diagram Walkthrough

flowchart LR
  A["findOneBy/findOne called<br/>with null/undefined"] --> B["hasValidWhereConditions<br/>checks conditions"]
  B --> C{All conditions<br/>null/undefined?}
  C -->|Yes| D["Return null<br/>immediately"]
  C -->|No| E["Execute query<br/>normally"]
  D --> F["Security issue<br/>prevented"]
  E --> G["Normal behavior<br/>preserved"]
Loading

File Walkthrough

Relevant files
Bug fix
EntityManager.ts
Add validation for null/undefined WHERE conditions             

src/entity-manager/EntityManager.ts

  • Added hasValidWhereConditions() private method to check if WHERE
    conditions contain at least one valid value
  • Method handles null/undefined values based on
    invalidWhereValuesBehavior configuration
  • Recursively checks nested objects and FindOperators
  • Modified findOne() to return null when all WHERE conditions are
    invalid
  • Modified findOneBy() to return null when all WHERE conditions are
    invalid
  • Updated JSDoc comments for update() and updateAll() methods to include
    options parameter
+85/-0   
Tests
find-options.test.ts
Add security tests for null/undefined condition handling 

test/functional/null-undefined-handling/find-options.test.ts

  • Added test for findOneBy() with all null conditions returning null
  • Added test for findOneBy() with all undefined conditions returning
    null
  • Added test for findOneBy() with mixed null/undefined conditions
    returning null
  • Added test for findOne() with all null/undefined conditions returning
    null
  • Added test for findOneByOrFail() throwing EntityNotFoundError with
    null/undefined conditions
  • Added test verifying normal behavior when at least one valid condition
    exists
+145/-0 

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Feb 13, 2026

PR Code Suggestions ✨

Latest suggestions up to 645b643

CategorySuggestion                                                                                                                                    Impact
Possible issue
Avoid recursing into non-plain objects

In hasValidWhereConditions, use ObjectUtils.isPlainObject to ensure recursion
only happens for nested where-objects, and not for other object types like Date
or ObjectId.

src/entity-manager/EntityManager.ts [1202-1264]

 private hasValidWhereConditions<Entity extends ObjectLiteral>(
     where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
 ): boolean {
     const invalidWhereValuesBehavior =
         this.connection.options.invalidWhereValuesBehavior || {}
     const nullBehavior = invalidWhereValuesBehavior.null || "ignore"
     const undefinedBehavior =
         invalidWhereValuesBehavior.undefined || "ignore"
 
-    // Helper to check if a single where object has valid conditions
     const hasValidConditionsInObject = (
         whereObj: FindOptionsWhere<Entity>,
     ): boolean => {
         for (const key in whereObj) {
             const value = whereObj[key]
 
-            // Check for undefined
             if (value === undefined) {
-                if (undefinedBehavior !== "ignore") {
-                    return true // Will be handled by buildWhere (throw or process)
-                }
+                if (undefinedBehavior !== "ignore") return true
                 continue
             }
 
-            // Check for null
             if (value === null) {
-                if (nullBehavior !== "ignore") {
-                    return true // Will be handled by buildWhere (sql-null or throw)
-                }
+                if (nullBehavior !== "ignore") return true
                 continue
             }
 
-            // Check for nested objects (embeds/relations)
-            if (typeof value === "object" && !Array.isArray(value)) {
-                // Use InstanceChecker for robust FindOperator detection
-                if (InstanceChecker.isFindOperator(value)) {
-                    return true
+            if (typeof value === "object" && value !== null && !Array.isArray(value)) {
+                if (InstanceChecker.isFindOperator(value)) return true
+
+                // Only recurse into plain objects (embeds/relations)
+                if (ObjectUtils.isPlainObject(value)) {
+                    if (
+                        this.hasValidWhereConditions([
+                            value as FindOptionsWhere<Entity>,
+                        ])
+                    ) {
+                        return true
+                    }
+                    continue
                 }
-                // Recursively check nested objects
-                const nested: FindOptionsWhere<Entity>[] = [
-                    value as FindOptionsWhere<Entity>,
-                ]
-                if (this.hasValidWhereConditions(nested)) {
-                    return true
-                }
-                continue
+
+                // Non-plain objects (Date/ObjectId/etc.) should be treated as valid values
+                return true
             }
 
-            // Any other value is valid
             return true
         }
         return false
     }
 
-    // Handle array of where conditions (OR logic)
     if (Array.isArray(where)) {
-        // At least one condition must be valid
         return where.some(hasValidConditionsInObject)
     }
 
-    // Single where object
     return hasValidConditionsInObject(where)
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug in the new hasValidWhereConditions method where non-plain objects like Date or ObjectId are incorrectly handled, which could cause queries with valid conditions to return null. The proposed fix using ObjectUtils.isPlainObject is accurate and prevents this functional regression.

Medium
Security
Iterate only own properties

Replace the for...in loop with for...of Object.keys() to iterate only over the
object's own properties, preventing potential issues from prototype pollution.

src/entity-manager/EntityManager.ts [1215-1216]

-for (const key in whereObj) {
+for (const key of Object.keys(whereObj)) {
     const value = whereObj[key]
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out a potential security risk with using a for...in loop without an hasOwnProperty check, which could lead to processing properties from the prototype chain. While prototype pollution is a valid concern, the context of this specific function slightly mitigates the immediate risk, making this a valuable but not critical improvement.

Medium
  • More

Previous suggestions

✅ Suggestions up to commit 42ecbf8
CategorySuggestion                                                                                                                                    Impact
High-level
Helper logic duplicates QueryBuilder behavior

The new hasValidWhereConditions helper duplicates WHERE clause processing logic
already in QueryBuilder, creating a maintenance risk. This check should be
delegated to the QueryBuilder or a shared utility to prevent future bugs from
logic divergence.

Examples:

src/entity-manager/EntityManager.ts [1202-1264]
    private hasValidWhereConditions<Entity extends ObjectLiteral>(
        where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
    ): boolean {
        const invalidWhereValuesBehavior =
            this.connection.options.invalidWhereValuesBehavior || {}
        const nullBehavior = invalidWhereValuesBehavior.null || "ignore"
        const undefinedBehavior =
            invalidWhereValuesBehavior.undefined || "ignore"

        // Helper to check if a single where object has valid conditions

 ... (clipped 53 lines)

Solution Walkthrough:

Before:

class EntityManager {
    private hasValidWhereConditions(where) {
        // Re-implements logic to check for null/undefined
        // based on connection options.
        const nullBehavior = this.connection.options...;
        for (const key in where) {
            const value = where[key];
            if (value === null && nullBehavior === "ignore") {
                continue; // Ignored value
            }
            // ... other checks for undefined, FindOperator, etc.
            return true; // Found a valid condition
        }
        return false; // No valid conditions
    }

    async findOneBy(entityClass, where) {
        if (!this.hasValidWhereConditions(where)) {
            return null;
        }
        return this.createQueryBuilder(...).getOne();
    }
}

After:

class EntityManager {
    async findOneBy(entityClass, where) {
        const qb = this.createQueryBuilder(entityClass, ...);
        qb.setFindOptions({ where, take: 1 });

        // Ideal: QueryBuilder exposes whether the WHERE clause is empty
        if (qb.whereExpressionIsEmpty()) { // Fictional method
            return null;
        }

        return qb.getOne();
    }
}

// Alternative: A shared utility function
// function hasValidWhereConditions(where, options) { ... }
// This utility would be used by both EntityManager and QueryBuilder
// to avoid logic duplication.
Suggestion importance[1-10]: 8

__

Why: This is a valid and significant architectural concern, as duplicating the WHERE clause validation logic from the QueryBuilder creates a maintenance risk and could lead to future bugs if the two implementations diverge.

Medium
Possible issue
Use a robust method for checks
Suggestion Impact:The commit replaced the constructor.name string check with InstanceChecker.isFindOperator(value), making FindOperator detection more robust.

code diff:

-                    // For FindOperators or nested where conditions
-                    if (
-                        value.constructor &&
-                        value.constructor.name === "FindOperator"
-                    ) {
+                    // Use InstanceChecker for robust FindOperator detection
+                    if (InstanceChecker.isFindOperator(value)) {
                         return true
                     }

Replace the string-based check value.constructor.name === "FindOperator" with
the more robust InstanceChecker.isFindOperator(value) method to avoid issues
with code minification.

src/entity-manager/EntityManager.ts [1237-1242]

-if (
-    value.constructor &&
-    value.constructor.name === "FindOperator"
-) {
+if (InstanceChecker.isFindOperator(value)) {
     return true
 }
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that checking constructor.name is not robust and proposes a safer alternative using InstanceChecker.isFindOperator(value), which improves code quality and maintainability.

Low

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Feb 13, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

✅ 1. hasValidWhereConditions uses as any 📘 Rule violation ✓ Correctness
Description
The new helper uses an as any cast when recursing into nested where objects, bypassing type
safety and potentially masking incorrect FindOptionsWhere shapes. This violates the requirement to
avoid any-casts and introduces maintenance risk.
Code

src/entity-manager/EntityManager.ts[1244]

+                    if (this.hasValidWhereConditions([value] as any)) {
Evidence
PR Compliance ID 4 forbids introducing any-casts to bypass type issues. The new code explicitly
casts [value] to any during recursion.

Rule 4: Remove AI-generated noise
src/entity-manager/EntityManager.ts[1237-1245]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hasValidWhereConditions()` currently uses an `as any` cast when recursing into nested where objects, which violates the project rule against type-bypassing casts.
## Issue Context
This helper was added as part of the security fix, but it should preserve TypeScript safety and avoid `any`.
## Fix Focus Areas
- src/entity-manager/EntityManager.ts[1234-1246]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Docs still claim first row 📘 Rule violation ✓ Correctness
Description
The PR changes findOne/findOneBy to return null when all WHERE conditions are null/undefined,
but the docs still say Repository.findOneBy({ id: undefined }) returns the first row. This is a
user-facing behavior change that needs documentation updates.
Code

src/entity-manager/EntityManager.ts[R1395-1400]

+        // Check if all WHERE conditions are null/undefined (would result in empty WHERE clause)
+        // This prevents the security issue where findOne({ where: { id: null } }) returns the first record
+        const hasValidConditions = this.hasValidWhereConditions(options.where)
+        if (!hasValidConditions) {
+            return null
+        }
Evidence
PR Compliance ID 2 requires documentation updates for user-facing changes. The code now returns
null for all-null/undefined where clauses, while the documentation still describes the old unsafe
behavior of returning the first row.

Rule 2: Docs updated for user-facing changes
src/entity-manager/EntityManager.ts[1395-1400]
docs/docs/data-source/5-null-and-undefined-handling.md[7-11]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Documentation still states that `Repository.findOneBy({ id: undefined })` returns the first row, but the PR changes this behavior to return `null` when all WHERE conditions are ignored.
## Issue Context
This is a user-facing behavioral change introduced to prevent unintended data exposure.
## Fix Focus Areas
- docs/docs/data-source/5-null-and-undefined-handling.md[1-20]
- src/entity-manager/EntityManager.ts[1395-1428]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


✅ 3. Date/ObjectId treated invalid 🐞 Bug ✓ Correctness
Description
hasValidWhereConditions treats any non-array object as a nested where-object unless its constructor
name is exactly "FindOperator". This can incorrectly classify valid scalar condition values like
Date/Buffer/ObjectId (and other non-plain objects) as having no valid conditions, causing
findOne/findOneBy to return null without executing a query.
Code

src/entity-manager/EntityManager.ts[R1234-1248]

+                // Check for nested objects (embeds/relations)
+                if (typeof value === "object" && !Array.isArray(value)) {
+                    // For FindOperators or nested where conditions
+                    if (
+                        value.constructor &&
+                        value.constructor.name === "FindOperator"
+                    ) {
+                        return true
+                    }
+                    // Recursively check nested objects
+                    if (this.hasValidWhereConditions([value] as any)) {
+                        return true
+                    }
+                    continue
+                }
Evidence
Type definitions explicitly allow Date/Buffer/ObjectId as scalar values in FindOptionsWhere, but the
new helper recurses into any object and returns false when it finds no enumerable keys—typical for
Date and many ObjectId implementations—triggering the early return null path. Meanwhile
SelectQueryBuilder.buildWhere treats column values (including objects) as parameter values based on
metadata, not by requiring them to be plain objects with keys.

src/entity-manager/EntityManager.ts[1234-1248]
src/find-options/FindOptionsWhere.ts[22-42]
src/query-builder/SelectQueryBuilder.ts[4469-4513]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`EntityManager.hasValidWhereConditions()` currently treats any `typeof value === &amp;quot;object&amp;quot;` (non-array) as a nested where object and recurses into it. This breaks valid `FindOptionsWhere` scalar values that are implemented as objects (e.g., `Date`, `Buffer`, Mongo `ObjectId`, and potentially other non-plain objects or object-typed columns), causing `findOne` / `findOneBy` to return `null` without querying.
### Issue Context
Type definitions allow `Date | Buffer | ObjectId` as valid where values, and `SelectQueryBuilder.buildWhere()` decides whether to recurse based on metadata (column/embed/relation), not on `typeof value`.
### Fix Focus Areas
- src/entity-manager/EntityManager.ts[1197-1264]
- src/entity-manager/EntityManager.ts[1377-1409]
- src/entity-manager/EntityManager.ts[1417-1437]
### Suggested approach
- Change `hasValidWhereConditions` to accept `metadata` (+ optional `embedPrefix`) and follow the same key classification as `SelectQueryBuilder.buildWhere`:
- If key resolves to a **column**, treat any non-null/undefined (subject to invalidWhereValuesBehavior) as valid, including `Date`, `Buffer`, `ObjectId`, and object literals.
- If key resolves to an **embed** or **relation**, recurse into the nested where.
- As a lighter-weight alternative (but less correct than metadata-based), treat **non-plain objects** as scalar values (e.g., via prototype checks like `Object.getPrototypeOf(value) !== Object.prototype`) to cover `Date`/`ObjectId`, while keeping plain-object recursion for nested where objects.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Throw config still leaks 🐞 Bug ⛨ Security
Description
With invalidWhereValuesBehavior.undefined = 'throw', the new guard treats undefined values as
“valid” (to let buildWhere throw), but buildWhere silently skips relation where-objects whose
properties are all undefined. This can still produce an empty WHERE clause and return the first row,
contradicting the PR’s stated behavior.
Code

src/entity-manager/EntityManager.ts[R1218-1223]

+                // Check for undefined
+                if (value === undefined) {
+                    if (undefinedBehavior !== "ignore") {
+                        return true // Will be handled by buildWhere (throw or process)
+                    }
+                    continue
Evidence
The new helper returns true when it encounters an undefined value and undefinedBehavior is not
"ignore". However, SelectQueryBuilder.buildWhere has a relation-specific short-circuit that skips
joining/where generation when all nested relation properties are undefined, and it does not consult
undefinedBehavior. So in cases like findOneBy({ relation: { id: undefined } }) with
undefinedBehavior='throw', the guard will allow query execution, buildWhere will skip the relation
condition, and the query can return the first record.

src/entity-manager/EntityManager.ts[1218-1223]
src/query-builder/SelectQueryBuilder.ts[4544-4552]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guard assumes buildWhere will throw when `invalidWhereValuesBehavior.undefined = &amp;#x27;throw&amp;#x27;`, and thus returns `true` on encountering `undefined`. But `SelectQueryBuilder.buildWhere` has a relation-specific shortcut that silently skips relation objects whose nested properties are all `undefined`, producing an empty WHERE and allowing first-row reads.
### Issue Context
This is specifically about **relation** keys (`metadata.findRelationWithPropertyPath(...)`) with nested objects like `{ relation: { id: undefined } }`.
### Fix Focus Areas
- src/entity-manager/EntityManager.ts[1205-1264]
- src/query-builder/SelectQueryBuilder.ts[4544-4552]
### Suggested fix options
1) **Safer/Minimal (guard-side):** When `undefinedBehavior === &amp;#x27;throw&amp;#x27;`, throw a `TypeORMError` from `hasValidWhereConditions` as soon as an undefined is detected anywhere in the where tree (including nested relation objects). This guarantees no empty-WHERE query can slip through.
2) **More correct (builder-side):** Adjust the relation `allAllUndefined` shortcut in `buildWhere` to respect `undefinedBehavior`:
 - If `&amp;#x27;throw&amp;#x27;`, throw the same error as the column branch.
 - If `&amp;#x27;ignore&amp;#x27;`, keep skipping.
Either approach should ensure PR’s stated behavior (“still throws”) holds for relation nested objects too.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

✅ 5. Brittle FindOperator check 🐞 Bug ⛯ Reliability
Description
The helper detects FindOperators via value.constructor.name === "FindOperator" instead of the
project’s established InstanceChecker.isFindOperator mechanism. This is inconsistent with the rest
of the codebase and is fragile under bundling/minification or alternate operator implementations.
Code

src/entity-manager/EntityManager.ts[R1236-1242]

+                    // For FindOperators or nested where conditions
+                    if (
+                        value.constructor &&
+                        value.constructor.name === "FindOperator"
+                    ) {
+                        return true
+                    }
Evidence
EntityManager already imports InstanceChecker and the query-building stack uses
InstanceChecker.isFindOperator (symbol-based @instanceof) to identify operators reliably,
including EqualOperator. Using constructor.name is less robust and diverges from existing patterns.

src/entity-manager/EntityManager.ts[1236-1242]
src/util/InstanceChecker.ts[92-96]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hasValidWhereConditions` uses `value.constructor.name === &amp;quot;FindOperator&amp;quot;` to detect operators, which is brittle and inconsistent with the rest of TypeORM.
### Issue Context
The project uses `InstanceChecker.isFindOperator()` (symbol-based `@instanceof`) across query builder code.
### Fix Focus Areas
- src/entity-manager/EntityManager.ts[1234-1243]
### Suggested change
- Replace the constructor-name logic with:
- `if (InstanceChecker.isFindOperator(value)) return true;`
This aligns behavior with `SelectQueryBuilder` / `QueryBuilder` and avoids relying on `constructor.name`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

return true
}
// Recursively check nested objects
if (this.hasValidWhereConditions([value] as any)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. hasvalidwhereconditions uses as any 📘 Rule violation ✓ Correctness

The new helper uses an as any cast when recursing into nested where objects, bypassing type
safety and potentially masking incorrect FindOptionsWhere shapes. This violates the requirement to
avoid any-casts and introduces maintenance risk.
Agent Prompt
## Issue description
`hasValidWhereConditions()` currently uses an `as any` cast when recursing into nested where objects, which violates the project rule against type-bypassing casts.

## Issue Context
This helper was added as part of the security fix, but it should preserve TypeScript safety and avoid `any`.

## Fix Focus Areas
- src/entity-manager/EntityManager.ts[1234-1246]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1395 to +1400
// Check if all WHERE conditions are null/undefined (would result in empty WHERE clause)
// This prevents the security issue where findOne({ where: { id: null } }) returns the first record
const hasValidConditions = this.hasValidWhereConditions(options.where)
if (!hasValidConditions) {
return null
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Docs still claim first row 📘 Rule violation ✓ Correctness

The PR changes findOne/findOneBy to return null when all WHERE conditions are null/undefined,
but the docs still say Repository.findOneBy({ id: undefined }) returns the first row. This is a
user-facing behavior change that needs documentation updates.
Agent Prompt
## Issue description
Documentation still states that `Repository.findOneBy({ id: undefined })` returns the first row, but the PR changes this behavior to return `null` when all WHERE conditions are ignored.

## Issue Context
This is a user-facing behavioral change introduced to prevent unintended data exposure.

## Fix Focus Areas
- docs/docs/data-source/5-null-and-undefined-handling.md[1-20]
- src/entity-manager/EntityManager.ts[1395-1428]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1234 to +1248
// Check for nested objects (embeds/relations)
if (typeof value === "object" && !Array.isArray(value)) {
// For FindOperators or nested where conditions
if (
value.constructor &&
value.constructor.name === "FindOperator"
) {
return true
}
// Recursively check nested objects
if (this.hasValidWhereConditions([value] as any)) {
return true
}
continue
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Date/objectid treated invalid 🐞 Bug ✓ Correctness

hasValidWhereConditions treats any non-array object as a nested where-object unless its constructor
name is exactly "FindOperator". This can incorrectly classify valid scalar condition values like
Date/Buffer/ObjectId (and other non-plain objects) as having no valid conditions, causing
findOne/findOneBy to return null without executing a query.
Agent Prompt
### Issue description
`EntityManager.hasValidWhereConditions()` currently treats any `typeof value === "object"` (non-array) as a nested where object and recurses into it. This breaks valid `FindOptionsWhere` scalar values that are implemented as objects (e.g., `Date`, `Buffer`, Mongo `ObjectId`, and potentially other non-plain objects or object-typed columns), causing `findOne` / `findOneBy` to return `null` without querying.

### Issue Context
Type definitions allow `Date | Buffer | ObjectId` as valid where values, and `SelectQueryBuilder.buildWhere()` decides whether to recurse based on metadata (column/embed/relation), not on `typeof value`.

### Fix Focus Areas
- src/entity-manager/EntityManager.ts[1197-1264]
- src/entity-manager/EntityManager.ts[1377-1409]
- src/entity-manager/EntityManager.ts[1417-1437]

### Suggested approach
- Change `hasValidWhereConditions` to accept `metadata` (+ optional `embedPrefix`) and follow the same key classification as `SelectQueryBuilder.buildWhere`:
  - If key resolves to a **column**, treat any non-null/undefined (subject to invalidWhereValuesBehavior) as valid, including `Date`, `Buffer`, `ObjectId`, and object literals.
  - If key resolves to an **embed** or **relation**, recurse into the nested where.
- As a lighter-weight alternative (but less correct than metadata-based), treat **non-plain objects** as scalar values (e.g., via prototype checks like `Object.getPrototypeOf(value) !== Object.prototype`) to cover `Date`/`ObjectId`, while keeping plain-object recursion for nested where objects.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1218 to +1223
// Check for undefined
if (value === undefined) {
if (undefinedBehavior !== "ignore") {
return true // Will be handled by buildWhere (throw or process)
}
continue

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Throw config still leaks 🐞 Bug ⛨ Security

With invalidWhereValuesBehavior.undefined = 'throw', the new guard treats undefined values as
“valid” (to let buildWhere throw), but buildWhere silently skips relation where-objects whose
properties are all undefined. This can still produce an empty WHERE clause and return the first row,
contradicting the PR’s stated behavior.
Agent Prompt
### Issue description
The guard assumes buildWhere will throw when `invalidWhereValuesBehavior.undefined = 'throw'`, and thus returns `true` on encountering `undefined`. But `SelectQueryBuilder.buildWhere` has a relation-specific shortcut that silently skips relation objects whose nested properties are all `undefined`, producing an empty WHERE and allowing first-row reads.

### Issue Context
This is specifically about **relation** keys (`metadata.findRelationWithPropertyPath(...)`) with nested objects like `{ relation: { id: undefined } }`.

### Fix Focus Areas
- src/entity-manager/EntityManager.ts[1205-1264]
- src/query-builder/SelectQueryBuilder.ts[4544-4552]

### Suggested fix options
1) **Safer/Minimal (guard-side):** When `undefinedBehavior === 'throw'`, throw a `TypeORMError` from `hasValidWhereConditions` as soon as an undefined is detected anywhere in the where tree (including nested relation objects). This guarantees no empty-WHERE query can slip through.

2) **More correct (builder-side):** Adjust the relation `allAllUndefined` shortcut in `buildWhere` to respect `undefinedBehavior`:
   - If `'throw'`, throw the same error as the column branch.
   - If `'ignore'`, keep skipping.

Either approach should ensure PR’s stated behavior (“still throws”) holds for relation nested objects too.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

…t, update docs

- Replace brittle constructor.name check with InstanceChecker.isFindOperator()
- Remove 'as any' cast by using proper generic type for nested recursion
- Update null/undefined handling docs to reflect new findOne/findOneBy behavior

Signed-off-by: pierreeurope <[email protected]>
@qodo-free-for-open-source-projects

Persistent review updated to latest commit 645b643

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

findOneBy({ id: null | undefined }) still returns first record in TypeORM 0.3.28

1 participant