Skip to content

fix(api): holistic prototype pollution guard in base-orm repository#1955

Open
anis-marrouchi wants to merge 2 commits into
hexabot-ai:mainfrom
anis-marrouchi:5-holistic-prototype-pollution-guard
Open

fix(api): holistic prototype pollution guard in base-orm repository#1955
anis-marrouchi wants to merge 2 commits into
hexabot-ai:mainfrom
anis-marrouchi:5-holistic-prototype-pollution-guard

Conversation

@anis-marrouchi

@anis-marrouchi anis-marrouchi commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Context

This is a follow-up to #1940 which addressed prototype pollution in search filters. While that PR hardened the query/filter path, the create and update paths in BaseOrmRepository were still unguarded. This PR closes that gap.

Closes #5

Test plan

  • 7 new test cases covering __proto__, constructor, prototype in create/createMany/updateOne/updateMany
  • Safe payload passthrough test to avoid false positives
  • Pre-commit hooks pass with related tests only

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Write operations (create, createMany, updateOne, updateMany) now enforce payload safety and reject requests containing forbidden property patterns, including nested keys and array-contained values.
  • Tests

    • Added tests verifying rejection of unsafe payloads (various nesting/array scenarios) and acceptance of valid payloads.

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a recursive payload-safety validation to BaseOrmRepository that flattens and inspects property paths for forbidden segments (__proto__, constructor, prototype) and throws BadRequestException when detected; validation is applied to create, createMany, updateOne, and updateMany. New tests cover root, nested, flattened, and array-contained forbidden keys and a safe-payload case.

Changes

Prototype-Pollution Guard

Layer / File(s) Summary
Validation Helper
packages/api/src/utils/generics/base-orm.repository.ts
Adds private assertSafePayload(data: unknown) that recursively traverses arrays/objects, flattens object keys, and checks each flattened path with hasForbiddenSegment, throwing BadRequestException on forbidden segments.
Wiring: Create Paths
packages/api/src/utils/generics/base-orm.repository.ts
Calls assertSafePayload(payload) at the start of create and createMany before DTO→entity conversion, hooks, or persistence.
Wiring: Update Paths
packages/api/src/utils/generics/base-orm.repository.ts
Calls assertSafePayload(payload) at the start of updateOne and updateMany (including updateOne with shouldFlatten: true) before fetching, applying updates, hooks, or persisting.
Tests: Prototype-Pollution
packages/api/src/utils/generics/base-orm.repository.spec.ts
New describe('prototype pollution guard') tests that repository write operations reject payloads containing __proto__, constructor, and prototype at root, nested, flattened, and inside arrays (including JSON-parsed arrays), plus a test confirming acceptance of a safe payload.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Repository as BaseOrmRepository
    participant Validator as assertSafePayload
    participant Persistence as DB/Storage

    Client->>Repository: call create/update with payload
    Repository->>Validator: assertSafePayload(payload)
    Validator->>Validator: flatten payload keys
    Validator->>Validator: check keys for forbidden segments
    alt Forbidden segment found
        Validator-->>Repository: throw BadRequestException
        Repository-->>Client: respond with BadRequest
    else All keys safe
        Validator-->>Repository: approval
        Repository->>Persistence: persist data
        Persistence-->>Repository: success
        Repository-->>Client: return result
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 I hop through keys both near and far,

proto and ctor, I sniff and spar.
I flatten paths, I check each part,
I toss out keys that break apart.
Safe writes bloom — a rabbit's guard.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR description states it closes issue #5, but the changes focus on prototype pollution guards in the API repository, not Docker configuration. Verify the correct linked issue. The current changes address prototype pollution (likely issue #1940 or another security issue), not the frontend Dockerfile mentioned in #5.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding prototype pollution protection to the base ORM repository.
Description check ✅ Passed The description covers all required template sections with clear summary, context, and test plan, though formatted more extensively than minimally required.
Out of Scope Changes check ✅ Passed All changes are focused on adding prototype pollution protection to BaseOrmRepository, which aligns with the PR objectives and security hardening goals.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 14 minutes and 56 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/api/src/utils/generics/base-orm.repository.ts (1)

144-154: Arrays containing forbidden keys are not validated.

The flatten() helper treats arrays as leaf values (per packages/api/src/utils/helpers/flatten.ts), so payloads like { items: [{ __proto__: {} }] } would pass validation. Additionally, if an array is passed directly, flatten() throws a raw Error rather than BadRequestException.

Consider recursively inspecting array elements:

♻️ Proposed enhancement
 private assertSafePayload(data: unknown): void {
-  if (!data || typeof data !== 'object') return;
+  if (!data || typeof data !== 'object') return;
+  if (Array.isArray(data)) {
+    data.forEach((item) => this.assertSafePayload(item));
+    return;
+  }

   const flatKeys = Object.keys(
     flatten(data as Record<string, unknown>) as Record<string, unknown>,
   );
   const forbidden = flatKeys.find((key) => hasForbiddenSegment(key));
   if (forbidden) {
     throw new BadRequestException(`Forbidden property path: "${forbidden}"`);
   }
+  // Recursively check array values
+  for (const value of Object.values(data)) {
+    if (Array.isArray(value)) {
+      value.forEach((item) => this.assertSafePayload(item));
+    }
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/utils/generics/base-orm.repository.ts` around lines 144 -
154, The assertSafePayload function currently relies on flatten(), which treats
arrays as leaves and can also throw a raw Error for top-level arrays; update
assertSafePayload to handle arrays explicitly by recursively iterating array
elements and validating each element (if element is an object, obtain its
flattened keys or recurse) and by catching any errors from flatten() and
rethrowing them as BadRequestException; reference the existing symbols
assertSafePayload, flatten, hasForbiddenSegment, and BadRequestException so the
fix: detect Array.isArray(data) and either recurse into elements or wrap
flatten() usage in try/catch to convert errors and then run hasForbiddenSegment
against every key found in nested objects and arrays to ensure forbidden
segments are rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/api/src/utils/generics/base-orm.repository.ts`:
- Around line 144-154: The assertSafePayload function currently relies on
flatten(), which treats arrays as leaves and can also throw a raw Error for
top-level arrays; update assertSafePayload to handle arrays explicitly by
recursively iterating array elements and validating each element (if element is
an object, obtain its flattened keys or recurse) and by catching any errors from
flatten() and rethrowing them as BadRequestException; reference the existing
symbols assertSafePayload, flatten, hasForbiddenSegment, and BadRequestException
so the fix: detect Array.isArray(data) and either recurse into elements or wrap
flatten() usage in try/catch to convert errors and then run hasForbiddenSegment
against every key found in nested objects and arrays to ensure forbidden
segments are rejected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46b862de-8885-427a-ae5e-c93549290bce

📥 Commits

Reviewing files that changed from the base of the PR and between acc18bc and 1ff93e3.

📒 Files selected for processing (2)
  • packages/api/src/utils/generics/base-orm.repository.spec.ts
  • packages/api/src/utils/generics/base-orm.repository.ts

@anis-marrouchi
anis-marrouchi force-pushed the 5-holistic-prototype-pollution-guard branch from 1ff93e3 to ede67aa Compare April 29, 2026 11:25
anis-marrouchi added a commit to anis-marrouchi/Hexabot that referenced this pull request Apr 30, 2026
flatten() treats arrays as leaf values, so forbidden keys inside array
elements (e.g. { items: [{ __proto__: {} }] }) bypassed the guard.
Iterate flattened entries and recurse into any array values.

Addresses CodeRabbit review nitpick on PR hexabot-ai#1955.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/api/src/utils/generics/base-orm.repository.spec.ts (1)

276-361: ⚡ Quick win

Assert the database stays unchanged when these guards fire.

These cases only verify that a BadRequestException is thrown. A partial-write bug in createMany or updateMany would still pass, so add a quick post-rejection count/lookup assertion to prove validation happens before persistence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/utils/generics/base-orm.repository.spec.ts` around lines 276
- 361, The tests only assert BadRequestException but don't verify no partial
writes occurred; after each expect(...rejects...) for methods
dummyRepository.create, createMany, updateOne, updateMany add a short
post-rejection assertion that the repository state is unchanged — e.g., for
create/createMany assert count() equals baselineEntities.length (or that no new
entity with the forbidden payload exists), and for updateOne/updateMany verify
the target entity (baselineEntities[0]) fields remain unchanged (or that total
count is unchanged) to prove validation runs before persistence; update the spec
blocks referencing dummyRepository.create, createMany, updateOne, updateMany and
baselineEntities accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/api/src/utils/generics/base-orm.repository.spec.ts`:
- Around line 276-361: The tests only assert BadRequestException but don't
verify no partial writes occurred; after each expect(...rejects...) for methods
dummyRepository.create, createMany, updateOne, updateMany add a short
post-rejection assertion that the repository state is unchanged — e.g., for
create/createMany assert count() equals baselineEntities.length (or that no new
entity with the forbidden payload exists), and for updateOne/updateMany verify
the target entity (baselineEntities[0]) fields remain unchanged (or that total
count is unchanged) to prove validation runs before persistence; update the spec
blocks referencing dummyRepository.create, createMany, updateOne, updateMany and
baselineEntities accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 880e844a-756c-4d3a-a5a5-42260029c7b1

📥 Commits

Reviewing files that changed from the base of the PR and between ede67aa and 6840963.

📒 Files selected for processing (2)
  • packages/api/src/utils/generics/base-orm.repository.spec.ts
  • packages/api/src/utils/generics/base-orm.repository.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/api/src/utils/generics/base-orm.repository.ts

anis-marrouchi and others added 2 commits May 2, 2026 10:43
Add assertSafePayload() check to create, createMany, updateOne, and
updateMany methods. Uses the existing hasForbiddenSegment helper to
reject __proto__, constructor, and prototype keys in flattened payloads.

Closes #5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
flatten() treats arrays as leaf values, so forbidden keys inside array
elements (e.g. { items: [{ __proto__: {} }] }) bypassed the guard.
Iterate flattened entries and recurse into any array values.

Addresses CodeRabbit review nitpick on PR hexabot-ai#1955.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@anis-marrouchi
anis-marrouchi force-pushed the 5-holistic-prototype-pollution-guard branch from 6840963 to ae115f9 Compare May 2, 2026 09:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/api/src/utils/generics/base-orm.repository.spec.ts (1)

311-321: 💤 Low value

Test title slightly misrepresents what is being verified.

assertSafePayload is invoked unconditionally before the shouldFlatten branch in updateOne (context snippet 4, line 350), so the guard fires the same way whether or not shouldFlatten: true is passed. The test validates that updateOne rejects a nested forbidden key (specifically dynamicField.__proto__), which is a useful and distinct case from Test 4's root-level __proto__, but the "in flatten mode" framing implies the option affects when or how the guard runs.

✏️ Suggested rename
-    it('should reject updateOne with nested forbidden key in flatten mode', async () => {
+    it('should reject updateOne with nested forbidden key', async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/api/src/utils/generics/base-orm.repository.spec.ts` around lines 311
- 321, The test title is misleading because assertSafePayload runs before the
shouldFlatten branch in updateOne, so the failure is due to a nested forbidden
key (dynamicField.__proto__) regardless of flattening; rename the spec
description in the it(...) call (currently 'should reject updateOne with nested
forbidden key in flatten mode') to something like 'should reject updateOne with
nested forbidden key (dynamicField.__proto__)' to accurately describe the
behavior tested and keep the test code (payload, dummyRepository.updateOne call
and options) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/api/src/utils/generics/base-orm.repository.spec.ts`:
- Around line 311-321: The test title is misleading because assertSafePayload
runs before the shouldFlatten branch in updateOne, so the failure is due to a
nested forbidden key (dynamicField.__proto__) regardless of flattening; rename
the spec description in the it(...) call (currently 'should reject updateOne
with nested forbidden key in flatten mode') to something like 'should reject
updateOne with nested forbidden key (dynamicField.__proto__)' to accurately
describe the behavior tested and keep the test code (payload,
dummyRepository.updateOne call and options) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f3a82d36-88fd-4635-b772-6c662c7b4b3f

📥 Commits

Reviewing files that changed from the base of the PR and between 6840963 and ae115f9.

📒 Files selected for processing (2)
  • packages/api/src/utils/generics/base-orm.repository.spec.ts
  • packages/api/src/utils/generics/base-orm.repository.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/api/src/utils/generics/base-orm.repository.ts

@marrouchi

Copy link
Copy Markdown
Contributor

Thank you @anis-marrouchi

If I'm not mistaken, prototype pollution could affect object fields not all the payload. Meaning that only TypeORM entity attributes that are decorated with @JsonColumn() are most likely to be a target of such a vulnerability.

I wonder if we could rather update the actionDtoToEntity() method to look into the metadata of the attribute in order to assess that sanitization is needed or not. I wonder also how TypeORM handles JSON column data, it may be that TypeORM has that protection mechanism already.

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.

2 participants