fix: prevent security issue where findOneBy with all null/undefined conditions returns first record#11984
Conversation
…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]>
User descriptionDescriptionFixes #11873 - Security issue where ProblemEven after PR #11332 added the When ALL conditions in a WHERE clause are null/undefined, they're all ignored, resulting in:
Example: // With default configuration:
const user = await userRepository.findOneBy({ id: null })
// Returns the FIRST user instead of null! 🔴 Security issueSolutionAdded validation in The fix:
Behavior
TestsAdded comprehensive tests covering:
All tests pass ✓ Pull-Request Checklist
Breaking ChangesNone. This fix makes the default behavior safer without breaking existing valid use cases. PR TypeBug fix, Tests Description
Diagram Walkthroughflowchart 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"]
|
| Relevant files | |||
|---|---|---|---|
| Bug fix |
| ||
| Tests |
|
PR Code Suggestions ✨Latest suggestions up to 645b643
Previous suggestions✅ Suggestions up to commit 42ecbf8
|
||||||||||||||||||||||||
Code Review by Qodo
✅ 1.
|
src/entity-manager/EntityManager.ts
Outdated
| return true | ||
| } | ||
| // Recursively check nested objects | ||
| if (this.hasValidWhereConditions([value] as any)) { |
There was a problem hiding this comment.
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
| // 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 | ||
| } |
There was a problem hiding this comment.
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
| // 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 | ||
| } |
There was a problem hiding this comment.
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
| // Check for undefined | ||
| if (value === undefined) { | ||
| if (undefinedBehavior !== "ignore") { | ||
| return true // Will be handled by buildWhere (throw or process) | ||
| } | ||
| continue |
There was a problem hiding this comment.
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]>
|
Persistent review updated to latest commit 645b643 |
Description
Fixes #11873 - Security issue where
findOneBy({ id: null })orfindOneBy({ id: undefined })returns the FIRST record instead of null.Problem
Even after PR #11332 added the
invalidWhereValuesBehaviorconfiguration, 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:
Example:
Solution
Added validation in
findOneBy(),findOne(), and related methods to detect when all WHERE conditions would be ignored. In this case, the methods now returnnullimmediately without executing a query.The fix:
hasValidWhereConditions()helper that checks if at least one condition is validfindOneBy()andfindOne()to return null when all conditions are invalidfindOneByOrFail()andfindOneOrFail()inherit the fix and throwEntityNotFoundErroras expectedBehavior
null(safe)invalidWhereValuesBehavior.null = 'throw': Still throws (respected)invalidWhereValuesBehavior.null = 'sql-null': Still converts to SQL NULL (respected)Tests
Added comprehensive tests covering:
findOneBy({ id: null })returns nullfindOneBy({ id: undefined })returns nullfindOneBy({ id: null, title: null })returns null (multiple conditions)findOne({ where: { id: null } })returns nullfindOneByOrFail({ id: null })throws EntityNotFoundErrorAll tests pass ✓
Pull-Request Checklist
masterbranchnpm run formatto apply prettier formattingFixes #11873Breaking Changes
None. This fix makes the default behavior safer without breaking existing valid use cases.