Skip to content

fix(server-core): enforce memory conversation ownership - #1388

Open
zcxGGmu wants to merge 3 commits into
VoltAgent:mainfrom
zcxGGmu:fix/issue-1371-memory-ownership-check
Open

fix(server-core): enforce memory conversation ownership#1388
zcxGGmu wants to merge 3 commits into
VoltAgent:mainfrom
zcxGGmu:fix/issue-1371-memory-ownership-check

Conversation

@zcxGGmu

@zcxGGmu zcxGGmu commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • Enforces memory conversation ownership for authenticated requests before returning conversations, listing messages, updating conversations, or deleting conversations.
  • Passes the authenticated user id from Hono and Elysia memory routes into the shared server-core handlers.
  • Adds regression coverage for cross-user memory conversation access attempts.

Test Plan

  • pnpm --filter @voltagent/server-core test -- --run
  • pnpm --filter @voltagent/server-core typecheck
  • pnpm --filter @voltagent/server-core --filter @voltagent/server-hono --filter @voltagent/server-elysia build
  • biome check packages/server-core/src/handlers/memory.handlers.ts packages/server-core/src/handlers/memory.handlers.spec.ts packages/server-hono/src/routes/memory.routes.ts packages/server-elysia/src/routes/memory.routes.ts

Fixes #1371


Summary by cubic

Enforces conversation ownership checks across memory APIs and storage adapters to block cross-user access (IDOR). Routes and handlers now pass and enforce requestingUserId, and guarded mutations return 403 on ownership mismatch. Fixes #1371.

  • Bug Fixes
    • Return 403 for cross-user reads, message lists, updates, and deletes in @voltagent/server-core; map ConversationOwnershipMismatchError to 403 and forbid empty authenticated identities when listing.
    • Pass the authenticated user from @voltagent/server-hono and @voltagent/server-elysia to handlers as requestingUserId (extracted from id/sub), and prefer it over client userId when listing.
    • Add ConversationMutationOptions.expectedUserId and enforce ownership on updateConversation/deleteConversation in @voltagent/core; pre-check owner in Memory.deleteConversation and keep vector cleanup best-effort for unguarded deletes.
    • Enforce guarded mutations in adapters: @voltagent/libsql, @voltagent/postgres, @voltagent/supabase, @voltagent/cloudflare-d1, and in-memory add user_id predicates and throw ConversationOwnershipMismatchError when no rows are affected.
    • @voltagent/voltagent-memory checks expected owner before delegating updates/deletes to the managed service.
    • Add tests for handler ownership checks, route propagation, and adapter SQL/query guards across packages.

Written for commit 96bcbab. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Security

    • Restricted memory conversation access and modifications to the conversation owner.
    • Unauthorized reads, message listings, updates, and deletions now return a 403 Forbidden response.
    • Memory operations use the authenticated identity, preventing client-supplied identity overrides.
    • Ownership is revalidated during updates and deletions to prevent unauthorized changes or data loss.
  • Tests

    • Added comprehensive coverage for owner access, unauthorized actions, identity handling, guarded mutations, and data preservation.

Reject authenticated memory conversation reads, message listing, updates, and deletes when the conversation belongs to another user.\n\nAdd regression tests for the IDOR paths reported in VoltAgent#1371 and pass authenticated user context from Hono and Elysia memory routes.
@changeset-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 96bcbab

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Memory routes now pass authenticated user identifiers to handlers. Handlers enforce ownership for listing, reads, message listing, updates, and deletes. Memory and storage adapters guard mutations with expected owner IDs. Tests cover unauthorized access, route propagation, storage predicates, and owner access.

Changes

Memory ownership authorization

Layer / File(s) Summary
Ownership contracts and guarded mutations
packages/core/src/memory/types.ts, packages/core/src/memory/errors.ts, packages/core/src/memory/index.ts, packages/core/src/memory/adapters/storage/in-memory.ts, packages/voltagent-memory/src/index.ts, packages/cloudflare-d1/src/memory-adapter.ts, packages/libsql/src/memory-core.ts, packages/postgres/src/memory-adapter.ts, packages/supabase/src/memory-adapter.ts
Conversation mutations accept expectedUserId. Memory services and storage adapters reject owner mismatches with ConversationOwnershipMismatchError.
Handler ownership gates
packages/server-core/src/handlers/memory.handlers.ts
Handlers use requestingUserId, ignore client-supplied listing identity, validate reads and messages, and return 403 for unauthorized mutations.
Authenticated identity propagation
packages/server-elysia/src/auth/middleware.ts, packages/server-elysia/src/routes/memory.routes.ts, packages/server-hono/src/routes/memory.routes.ts
Routes derive authenticated user IDs from id or sub claims and pass them to memory handlers.
Ownership behavior tests
packages/server-core/src/handlers/memory.handlers.spec.ts, packages/server-elysia/src/routes/memory.routes.spec.ts, packages/server-hono/src/routes/memory.routes.spec.ts, packages/cloudflare-d1/src/memory-adapter.spec.ts, packages/libsql/src/memory-v2-adapter.spec.ts, packages/postgres/src/memory-adapter.spec.ts, packages/supabase/src/memory-adapter.spec.ts, packages/core/src/memory/index.spec.ts, packages/voltagent-memory/src/index.spec.ts
Tests cover forbidden operations, authenticated listing filters, guarded SQL mutations, preserved data, owner retrieval, delegated ownership checks, and deletion after vector-read failure.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MemoryRoutes
  participant MemoryHandlers
  participant Memory
  participant StorageAdapters
  Client->>MemoryRoutes: Request conversation operation
  MemoryRoutes->>MemoryRoutes: Extract authenticated user ID
  MemoryRoutes->>MemoryHandlers: Pass requestingUserId
  MemoryHandlers->>Memory: Fetch or mutate conversation
  Memory->>StorageAdapters: Validate expectedUserId
  StorageAdapters-->>Memory: Return result or ownership mismatch
  Memory-->>MemoryHandlers: Return result or ownership error
  MemoryHandlers-->>MemoryRoutes: Return result or 403 Forbidden
  MemoryRoutes-->>Client: HTTP response
Loading

Possibly related issues

Possibly related PRs

  • VoltAgent/voltagent#974: Introduced the memory HTTP handlers and routes extended by this ownership enforcement change.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #1371 by enforcing authenticated ownership for reads, listings, updates, and deletes across routes, handlers, and storage adapters.
Out of Scope Changes check ✅ Passed The changes and tests support the ownership objective, including authentication propagation, guarded mutations, and vector cleanup behavior.
Title check ✅ Passed The title clearly and concisely describes the main change: enforcing memory conversation ownership in server-core.
Description check ✅ Passed The description explains the behavior change, affected routes, tests, linked issue, and reviewer context, despite not using every template heading.
✨ 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

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

@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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/server-core/src/handlers/memory.handlers.spec.ts`:
- Around line 120-128: Add a test for the conversation-list handler covering an
authenticated Bob requesting Alice’s userId through /api/memory/conversations,
and assert Alice’s conversation is excluded from the response before the
existing ownership tests. Reuse the existing test fixtures, dependencies,
handler symbol, and response shape in memory.handlers.spec.ts; keep the current
known-conversation message-listing test unchanged.

In `@packages/server-core/src/handlers/memory.handlers.ts`:
- Around line 568-581: Make updateConversation and deleteConversation use
storage-level mutations guarded by the verified owner via an expectedUserId
condition, rather than relying only on the preceding
getConversation/assertConversationOwner check. In
packages/server-core/src/handlers/memory.handlers.ts lines 568-581 and 614-627,
return 403 when the conditional update or delete affects no owned record, while
preserving the existing 404 response for a missing conversation.

In `@packages/server-elysia/src/routes/memory.routes.ts`:
- Around line 44-53: Update the list-conversations handlers in
packages/server-elysia/src/routes/memory.routes.ts (lines 44-53) and
packages/server-hono/src/routes/memory.routes.ts (lines 44-53) to derive the
authenticated identity via getAuthenticatedUserId and pass it as
requestingUserId to handleListMemoryConversations; remove use of the
client-controlled query.userId while preserving the existing route behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 60ab5139-c2e8-4847-9ff8-4578c2c97f9a

📥 Commits

Reviewing files that changed from the base of the PR and between 3377f6d and 6e64a3c.

📒 Files selected for processing (4)
  • packages/server-core/src/handlers/memory.handlers.spec.ts
  • packages/server-core/src/handlers/memory.handlers.ts
  • packages/server-elysia/src/routes/memory.routes.ts
  • packages/server-hono/src/routes/memory.routes.ts

Comment thread packages/server-core/src/handlers/memory.handlers.spec.ts
Comment thread packages/server-core/src/handlers/memory.handlers.ts
Comment thread packages/server-elysia/src/routes/memory.routes.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/server-core/src/handlers/memory.handlers.ts Outdated
Comment thread packages/server-elysia/src/routes/memory.routes.ts Outdated
Comment thread packages/server-elysia/src/routes/memory.routes.ts Outdated
Comment thread packages/server-core/src/handlers/memory.handlers.spec.ts
Comment thread packages/server-core/src/handlers/memory.handlers.ts
Comment thread packages/server-core/src/handlers/memory.handlers.spec.ts Outdated
@zcxGGmu

zcxGGmu commented Aug 2, 2026

Copy link
Copy Markdown
Author

Thanks for the review — I pushed 95ba4d94 addressing the remaining memory ownership feedback.

Changes made:

  • Treat an explicitly empty authenticated user id as an ownership mismatch instead of bypassing the check.
  • List memory conversations by the authenticated user id when present, ignoring a client-supplied userId for authenticated requests.
  • Pass authenticated identity through the Elysia and Hono list-conversations routes.
  • Move Elysia memory route ownership lookups to request-scoped auth state instead of reading the shared store directly.
  • Add storage-level expectedUserId guards for conversation update/delete and map guarded mutation misses to 403 while preserving 404 for missing conversations.
  • Import InMemoryStorageAdapter from the public @voltagent/core entry in the regression tests.

Validation:

  • npm exec -- vitest run src/handlers/memory.handlers.spec.ts --typecheck in packages/server-core — passed, 9 tests passed, no type errors.
  • npm exec -- vitest run src/routes/memory.routes.spec.ts --typecheck in packages/server-elysia — passed, 1 test passed, no type errors.
  • npm exec -- vitest run src/routes/memory.routes.spec.ts --typecheck in packages/server-hono — passed, 1 test passed, no type errors.
  • npm exec -- biome check <changed files> — passed.
  • npm run typecheck && npm run build in packages/core — passed.
  • npm run typecheck && npm run build in packages/server-core — passed.
  • npm run build in packages/server-elysia and packages/server-hono — passed.

Note: package-wide npm run typecheck in packages/server-elysia / packages/server-hono still reports existing unrelated route typing errors outside this memory-route change; the new focused route tests typecheck cleanly.

@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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/server-elysia/src/routes/memory.routes.ts (1)

117-130: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Enforce conversation ownership for working-memory, clone, update-working-memory, delete-messages, and search routes.

These routes still pass only client-supplied IDs/userId. The handlers only reject another user when userId is explicitly provided; they do not accept or reject based on the authenticated requestingUserId, so a non-owner can fetch, update, clone, delete, or search that conversation’s memory data by supplying the conversation ID. Add requestingUserId guards before the operation, with userId checked against the conversation owner where required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server-elysia/src/routes/memory.routes.ts` around lines 117 - 130,
Enforce authenticated ownership across the working-memory, clone,
update-working-memory, delete-messages, and search route handlers by obtaining
the authenticated requestingUserId and passing it into the corresponding memory
operations before they execute. Validate that requestingUserId owns the
conversation, and where a client-supplied userId is accepted, require it to
match the conversation owner; reject unauthorized requests consistently while
preserving existing success and HTTP status handling.
🧹 Nitpick comments (2)
packages/server-core/src/handlers/memory.handlers.spec.ts (1)

143-156: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a list-handler test for an explicit empty requestingUserId.

handleListMemoryConversations uses query.requestingUserId ?? query.userId, a different code path than assertConversationOwner used by handleGetMemoryConversation. The empty-string case tested at lines 133-141 for reads doesn't cover this path. Add a test that passes requestingUserId: "" to handleListMemoryConversations and asserts it returns no conversations, to lock in the "explicitly empty identity" contract across all ownership-sensitive handlers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server-core/src/handlers/memory.handlers.spec.ts` around lines 143 -
156, Add a test alongside the existing handleListMemoryConversations tests that
passes requestingUserId as an explicit empty string while retaining the
authenticated user setup. Assert the handler succeeds with zero total
conversations and an empty conversations list, covering the
query.requestingUserId ?? query.userId path without altering the existing
non-empty identity test.
packages/server-elysia/src/routes/memory.routes.ts (1)

45-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated getAuthenticatedUserId helper.

This function is duplicated verbatim in packages/server-hono/src/routes/memory.routes.ts. Since it implements the exact claim-extraction logic that this PR's ownership enforcement depends on, keeping one shared implementation in @voltagent/server-core reduces the risk of the two copies drifting and silently weakening authorization in one framework but not the other.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server-elysia/src/routes/memory.routes.ts` around lines 45 - 58,
Move the duplicated getAuthenticatedUserId helper into the shared
`@voltagent/server-core` package, export it, and replace the local implementations
in the Elysia and Hono memory routes with that shared import. Preserve the
existing id-then-sub claim extraction and undefined fallback behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/memory/types.ts`:
- Around line 447-452: Update updateConversation and deleteConversation across
D1MemoryAdapter, PostgreSQLMemoryAdapter, LibSQLMemoryCore,
SupabaseMemoryAdapter, and ManagedMemoryAdapter to accept and enforce
ConversationMutationOptions.expectedUserId. Use a single atomic
ownership-filtered UPDATE or DELETE statement keyed by both conversation id and
expectedUserId, and preserve the existing behavior for calls without an expected
user ID.

---

Outside diff comments:
In `@packages/server-elysia/src/routes/memory.routes.ts`:
- Around line 117-130: Enforce authenticated ownership across the
working-memory, clone, update-working-memory, delete-messages, and search route
handlers by obtaining the authenticated requestingUserId and passing it into the
corresponding memory operations before they execute. Validate that
requestingUserId owns the conversation, and where a client-supplied userId is
accepted, require it to match the conversation owner; reject unauthorized
requests consistently while preserving existing success and HTTP status
handling.

---

Nitpick comments:
In `@packages/server-core/src/handlers/memory.handlers.spec.ts`:
- Around line 143-156: Add a test alongside the existing
handleListMemoryConversations tests that passes requestingUserId as an explicit
empty string while retaining the authenticated user setup. Assert the handler
succeeds with zero total conversations and an empty conversations list, covering
the query.requestingUserId ?? query.userId path without altering the existing
non-empty identity test.

In `@packages/server-elysia/src/routes/memory.routes.ts`:
- Around line 45-58: Move the duplicated getAuthenticatedUserId helper into the
shared `@voltagent/server-core` package, export it, and replace the local
implementations in the Elysia and Hono memory routes with that shared import.
Preserve the existing id-then-sub claim extraction and undefined fallback
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 510b6c75-7d80-4689-bf86-c0025a60f667

📥 Commits

Reviewing files that changed from the base of the PR and between 6e64a3c and 95ba4d9.

📒 Files selected for processing (11)
  • packages/core/src/memory/adapters/storage/in-memory.ts
  • packages/core/src/memory/errors.ts
  • packages/core/src/memory/index.ts
  • packages/core/src/memory/types.ts
  • packages/server-core/src/handlers/memory.handlers.spec.ts
  • packages/server-core/src/handlers/memory.handlers.ts
  • packages/server-elysia/src/auth/middleware.ts
  • packages/server-elysia/src/routes/memory.routes.spec.ts
  • packages/server-elysia/src/routes/memory.routes.ts
  • packages/server-hono/src/routes/memory.routes.spec.ts
  • packages/server-hono/src/routes/memory.routes.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server-hono/src/routes/memory.routes.ts

Comment thread packages/core/src/memory/types.ts

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/memory/types.ts">

<violation number="1" location="packages/core/src/memory/types.ts:452">
P1: Persistent storage adapters ignore the new `expectedUserId` option, so the ownership check is not enforced at the storage boundary for update/delete operations. Updating every adapter to apply the user ID atomically, or enforcing the check centrally before delegation, would prevent a race or direct `Memory` caller from mutating another user’s conversation.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

options?: ConversationMutationOptions,
): Promise<Conversation>;
deleteConversation(id: string): Promise<void>;
deleteConversation(id: string, options?: ConversationMutationOptions): Promise<void>;

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.

P1: Persistent storage adapters ignore the new expectedUserId option, so the ownership check is not enforced at the storage boundary for update/delete operations. Updating every adapter to apply the user ID atomically, or enforcing the check centrally before delegation, would prevent a race or direct Memory caller from mutating another user’s conversation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/memory/types.ts, line 452:

<comment>Persistent storage adapters ignore the new `expectedUserId` option, so the ownership check is not enforced at the storage boundary for update/delete operations. Updating every adapter to apply the user ID atomically, or enforcing the check centrally before delegation, would prevent a race or direct `Memory` caller from mutating another user’s conversation.</comment>

<file context>
@@ -443,8 +447,9 @@ export interface StorageAdapter {
+    options?: ConversationMutationOptions,
   ): Promise<Conversation>;
-  deleteConversation(id: string): Promise<void>;
+  deleteConversation(id: string, options?: ConversationMutationOptions): Promise<void>;
 
   saveConversationSteps?(steps: ConversationStepRecord[]): Promise<void>;
</file context>

Comment thread packages/server-core/src/handlers/memory.handlers.ts
Comment thread packages/core/src/memory/index.ts
Apply expectedUserId checks to persistent conversation mutations, preserve best-effort vector cleanup for unguarded deletes, and reject empty authenticated identities when listing conversations.
@zcxGGmu

zcxGGmu commented Aug 3, 2026

Copy link
Copy Markdown
Author

Thanks for the review — pushed 96bcbabd3 with the ownership follow-ups:

  • Applies expectedUserId guards to persistent conversation update/delete mutations across D1, LibSQL, PostgreSQL, Supabase, and managed memory.
  • Keeps unguarded vector cleanup best-effort by moving the vector-only conversation lookup back inside the catchable path.
  • Rejects an empty authenticated requestingUserId before list-conversation queries can treat it as an omitted filter.
  • Adds regression coverage for the adapter mutation guards, empty identity listing, and unguarded vector-delete fallback.

Validation passed:

  • pnpm exec vitest run packages/server-core/src/handlers/memory.handlers.spec.ts packages/core/src/memory/index.spec.ts packages/cloudflare-d1/src/memory-adapter.spec.ts packages/libsql/src/memory-v2-adapter.spec.ts packages/postgres/src/memory-adapter.spec.ts packages/supabase/src/memory-adapter.spec.ts packages/voltagent-memory/src/index.spec.ts --reporter=default — 7 files / 78 tests passed.
  • pnpm --filter @voltagent/core typecheck
  • pnpm --filter @voltagent/server-core typecheck
  • pnpm --filter @voltagent/cloudflare-d1 build
  • pnpm --filter @voltagent/libsql build
  • pnpm --filter @voltagent/postgres build
  • pnpm --filter @voltagent/supabase build
  • pnpm --filter @voltagent/voltagent-memory build
  • pnpm exec biome check on the changed files

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/voltagent-memory/src/index.spec.ts`:
- Around line 68-110: Expand the mutation ownership tests around
ManagedMemoryAdapter.updateConversation and deleteConversation to cover both
outcomes: add an owner-matched update case asserting conversations.update is
delegated with the expected arguments, and add a mismatched-owner delete case
asserting ConversationOwnershipMismatchError and that conversations.delete is
not called.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc96dae9-7ee0-48b5-8b55-39f964b86a35

📥 Commits

Reviewing files that changed from the base of the PR and between 95ba4d9 and 96bcbab.

📒 Files selected for processing (14)
  • packages/cloudflare-d1/src/memory-adapter.spec.ts
  • packages/cloudflare-d1/src/memory-adapter.ts
  • packages/core/src/memory/index.spec.ts
  • packages/core/src/memory/index.ts
  • packages/libsql/src/memory-core.ts
  • packages/libsql/src/memory-v2-adapter.spec.ts
  • packages/postgres/src/memory-adapter.spec.ts
  • packages/postgres/src/memory-adapter.ts
  • packages/server-core/src/handlers/memory.handlers.spec.ts
  • packages/server-core/src/handlers/memory.handlers.ts
  • packages/supabase/src/memory-adapter.spec.ts
  • packages/supabase/src/memory-adapter.ts
  • packages/voltagent-memory/src/index.spec.ts
  • packages/voltagent-memory/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/server-core/src/handlers/memory.handlers.spec.ts
  • packages/core/src/memory/index.ts
  • packages/server-core/src/handlers/memory.handlers.ts

Comment on lines +68 to +110
it("rejects guarded updates before delegating when the owner does not match", async () => {
const { client, conversations } = createVoltOpsClient();
conversations.get.mockResolvedValue({
id: "conv-1",
userId: "user-2",
resourceId: "agent-1",
title: "Private",
metadata: {},
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
});

const adapter = new ManagedMemoryAdapter({ databaseId: "db-1", voltOpsClient: client });

await expect(
adapter.updateConversation("conv-1", { title: "Updated" }, { expectedUserId: "user-1" }),
).rejects.toBeInstanceOf(ConversationOwnershipMismatchError);

expect(conversations.update).not.toHaveBeenCalled();
});

it("checks ownership before delegated deletes", async () => {
const { client, conversations } = createVoltOpsClient();
conversations.get.mockResolvedValue({
id: "conv-1",
userId: "user-1",
resourceId: "agent-1",
title: "Private",
metadata: {},
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
});
conversations.delete.mockResolvedValue(undefined);

const adapter = new ManagedMemoryAdapter({ databaseId: "db-1", voltOpsClient: client });

await expect(
adapter.deleteConversation("conv-1", { expectedUserId: "user-1" }),
).resolves.toBeUndefined();

expect(conversations.get).toHaveBeenCalledWith("db-1", "conv-1");
expect(conversations.delete).toHaveBeenCalledWith("db-1", "conv-1");
});

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Cover both ownership outcomes for each mutation.

Lines 68-87 test only a denied update. Add an owner-matched update test that verifies conversations.update is called.

Lines 89-110 test only an allowed delete. Add a mismatched-owner delete test that verifies ConversationOwnershipMismatchError and that conversations.delete is not called.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/voltagent-memory/src/index.spec.ts` around lines 68 - 110, Expand
the mutation ownership tests around ManagedMemoryAdapter.updateConversation and
deleteConversation to cover both outcomes: add an owner-matched update case
asserting conversations.update is delegated with the expected arguments, and add
a mismatched-owner delete case asserting ConversationOwnershipMismatchError and
that conversations.delete is not called.

@cubic-dev-ai cubic-dev-ai 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.

8 issues found across 14 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/memory/index.ts">

<violation number="1" location="packages/core/src/memory/index.ts:310">
P1: A guarded delete can delete another owner’s message vectors even though the conversation delete is later rejected: when the initial ownership read returns null, this re-fetch may find a newly created same-ID conversation owned by someone else. Skip the vector-cleanup lookup when `expectedUserId` is set and the ownership lookup returned no conversation, so cleanup cannot run before the guarded mutation establishes ownership.</violation>
</file>

<file name="packages/libsql/src/memory-v2-adapter.spec.ts">

<violation number="1" location="packages/libsql/src/memory-v2-adapter.spec.ts:155">
P2: Cross-user mutation rejection is not covered here: these tests can pass even if ownership checks stop throwing. Adding cases with a different existing owner for update and `rowsAffected: 0` for delete, asserting `ConversationOwnershipMismatchError`, would protect the security behavior this PR introduces.</violation>
</file>

<file name="packages/cloudflare-d1/src/memory-adapter.ts">

<violation number="1" location="packages/cloudflare-d1/src/memory-adapter.ts:1106">
P2: Authenticated conversation deletion can permanently remove messages or steps while leaving the conversation behind if a later D1 write fails. Group the child and ownership-filtered parent deletes in one D1 batch/transaction, or rely on the existing parent cascades, so the operation cannot commit partially.</violation>
</file>

<file name="packages/postgres/src/memory-adapter.ts">

<violation number="1" location="packages/postgres/src/memory-adapter.ts:11">
P1: Installing this adapter with an older `@voltagent/core` still allowed by its `^2.0.0` peer range can now fail at module load because the new value import is missing. The package peer minimum should be raised to the first core release exporting `ConversationOwnershipMismatchError` (and the corresponding release metadata updated).</violation>
</file>

<file name="packages/core/src/memory/index.spec.ts">

<violation number="1" location="packages/core/src/memory/index.spec.ts:42">
P3: A failed assertion leaves `console.warn` mocked, which can suppress diagnostics and affect later tests. Restoring the spy in an `afterEach` hook or a `finally` block would keep this regression test isolated.</violation>
</file>

<file name="packages/voltagent-memory/src/index.ts">

<violation number="1" location="packages/voltagent-memory/src/index.ts:184">
P2: An explicitly empty authenticated identity is accepted when the conversation also has an empty `userId`, bypassing the intended empty-identity ownership rejection. Treat `expectedUserId === ""` as a mismatch before comparing owners.</violation>

<violation number="2" location="packages/voltagent-memory/src/index.ts:380">
P1: An ownership check can become stale before the managed-memory mutation executes: the adapter reads the conversation owner and then performs an unscoped remote update/delete. If ownership changes between those calls, the request can modify or delete the conversation after it belongs to another user. Passing the expected owner through to an atomic conditional update/delete (or rechecking ownership in the managed-memory service) would close this TOCTOU gap.</violation>
</file>

<file name="packages/cloudflare-d1/src/memory-adapter.spec.ts">

<violation number="1" location="packages/cloudflare-d1/src/memory-adapter.spec.ts:96">
P3: The new adapter tests do not verify the security-critical failure path: a guarded update or delete must reject when the owner does not match or when the database affects zero rows. Adding negative tests for each adapter, including assertions that the conversation and child data remain unchanged, would protect the revalidation behavior claimed by this change.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

try {
// Try to get the conversation first to get userId
const conversation = await this.storage.getConversation(id);
conversation ??= await this.storage.getConversation(id);

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.

P1: A guarded delete can delete another owner’s message vectors even though the conversation delete is later rejected: when the initial ownership read returns null, this re-fetch may find a newly created same-ID conversation owned by someone else. Skip the vector-cleanup lookup when expectedUserId is set and the ownership lookup returned no conversation, so cleanup cannot run before the guarded mutation establishes ownership.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/memory/index.ts, line 310:

<comment>A guarded delete can delete another owner’s message vectors even though the conversation delete is later rejected: when the initial ownership read returns null, this re-fetch may find a newly created same-ID conversation owned by someone else. Skip the vector-cleanup lookup when `expectedUserId` is set and the ownership lookup returned no conversation, so cleanup cannot run before the guarded mutation establishes ownership.</comment>

<file context>
@@ -296,21 +296,19 @@ export class Memory {
     // If vector adapter is configured, delete associated vectors
     if (this.vector) {
       try {
+        conversation ??= await this.storage.getConversation(id);
+
         if (conversation) {
</file context>
Suggested change
conversation ??= await this.storage.getConversation(id);
if (options?.expectedUserId === undefined) {
conversation = await this.storage.getConversation(id);
}

import {
ConversationAlreadyExistsError,
ConversationNotFoundError,
ConversationOwnershipMismatchError,

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.

P1: Installing this adapter with an older @voltagent/core still allowed by its ^2.0.0 peer range can now fail at module load because the new value import is missing. The package peer minimum should be raised to the first core release exporting ConversationOwnershipMismatchError (and the corresponding release metadata updated).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/postgres/src/memory-adapter.ts, line 11:

<comment>Installing this adapter with an older `@voltagent/core` still allowed by its `^2.0.0` peer range can now fail at module load because the new value import is missing. The package peer minimum should be raised to the first core release exporting `ConversationOwnershipMismatchError` (and the corresponding release metadata updated).</comment>

<file context>
@@ -5,9 +5,14 @@
+import {
+  ConversationAlreadyExistsError,
+  ConversationNotFoundError,
+  ConversationOwnershipMismatchError,
+} from "@voltagent/core";
 import type {
</file context>

updates: Partial<Omit<Conversation, "id" | "createdAt" | "updatedAt">>,
options?: ConversationMutationOptions,
): Promise<Conversation> {
await this.assertExpectedConversationOwner(id, options);

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.

P1: An ownership check can become stale before the managed-memory mutation executes: the adapter reads the conversation owner and then performs an unscoped remote update/delete. If ownership changes between those calls, the request can modify or delete the conversation after it belongs to another user. Passing the expected owner through to an atomic conditional update/delete (or rechecking ownership in the managed-memory service) would close this TOCTOU gap.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/voltagent-memory/src/index.ts, line 380:

<comment>An ownership check can become stale before the managed-memory mutation executes: the adapter reads the conversation owner and then performs an unscoped remote update/delete. If ownership changes between those calls, the request can modify or delete the conversation after it belongs to another user. Passing the expected owner through to an atomic conditional update/delete (or rechecking ownership in the managed-memory service) would close this TOCTOU gap.</comment>

<file context>
@@ -351,10 +372,13 @@ export class ManagedMemoryAdapter implements StorageAdapter {
     updates: Partial<Omit<Conversation, "id" | "createdAt" | "updatedAt">>,
+    options?: ConversationMutationOptions,
   ): Promise<Conversation> {
+    await this.assertExpectedConversationOwner(id, options);
+
     return this.withClientContext(({ client, database }) => {
</file context>

.mockResolvedValueOnce({ rows: [], rowsAffected: 1 })
.mockResolvedValueOnce({ rows: [{ ...existing, title: "Updated" }] });

await (adapter as any).updateConversation(

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.

P2: Cross-user mutation rejection is not covered here: these tests can pass even if ownership checks stop throwing. Adding cases with a different existing owner for update and rowsAffected: 0 for delete, asserting ConversationOwnershipMismatchError, would protect the security behavior this PR introduces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/libsql/src/memory-v2-adapter.spec.ts, line 155:

<comment>Cross-user mutation rejection is not covered here: these tests can pass even if ownership checks stop throwing. Adding cases with a different existing owner for update and `rowsAffected: 0` for delete, asserting `ConversationOwnershipMismatchError`, would protect the security behavior this PR introduces.</comment>

<file context>
@@ -135,4 +135,43 @@ describe.sequential("LibSQLMemoryAdapter - Advanced Behavior", () => {
+      .mockResolvedValueOnce({ rows: [], rowsAffected: 1 })
+      .mockResolvedValueOnce({ rows: [{ ...existing, title: "Updated" }] });
+
+    await (adapter as any).updateConversation(
+      "conv-1",
+      { title: "Updated" },
</file context>

const stepsTable = `${this.tablePrefix}_steps`;

if (options?.expectedUserId !== undefined) {
await this.run(

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.

P2: Authenticated conversation deletion can permanently remove messages or steps while leaving the conversation behind if a later D1 write fails. Group the child and ownership-filtered parent deletes in one D1 batch/transaction, or rely on the existing parent cascades, so the operation cannot commit partially.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cloudflare-d1/src/memory-adapter.ts, line 1106:

<comment>Authenticated conversation deletion can permanently remove messages or steps while leaving the conversation behind if a later D1 write fails. Group the child and ownership-filtered parent deletes in one D1 batch/transaction, or rely on the existing parent cascades, so the operation cannot commit partially.</comment>

<file context>
@@ -1066,26 +1073,54 @@ export class D1MemoryAdapter implements StorageAdapter {
     const stepsTable = `${this.tablePrefix}_steps`;
 
+    if (options?.expectedUserId !== undefined) {
+      await this.run(
+        `DELETE FROM ${messagesTable} WHERE conversation_id = ? AND EXISTS (SELECT 1 FROM ${conversationsTable} WHERE id = ? AND user_id = ?)`,
+        [id, id, options.expectedUserId],
</file context>

throw new ConversationNotFoundError(id);
}

if (conversation.userId !== options.expectedUserId) {

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.

P2: An explicitly empty authenticated identity is accepted when the conversation also has an empty userId, bypassing the intended empty-identity ownership rejection. Treat expectedUserId === "" as a mismatch before comparing owners.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/voltagent-memory/src/index.ts, line 184:

<comment>An explicitly empty authenticated identity is accepted when the conversation also has an empty `userId`, bypassing the intended empty-identity ownership rejection. Treat `expectedUserId === ""` as a mismatch before comparing owners.</comment>

<file context>
@@ -165,6 +168,24 @@ export class ManagedMemoryAdapter implements StorageAdapter {
+      throw new ConversationNotFoundError(id);
+    }
+
+    if (conversation.userId !== options.expectedUserId) {
+      throw new ConversationOwnershipMismatchError(id);
+    }
</file context>
Suggested change
if (conversation.userId !== options.expectedUserId) {
if (options.expectedUserId === "" || conversation.userId !== options.expectedUserId) {

);
await expect(storage.getConversation("conv-1")).resolves.toBeNull();

warnSpy.mockRestore();

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.

P3: A failed assertion leaves console.warn mocked, which can suppress diagnostics and affect later tests. Restoring the spy in an afterEach hook or a finally block would keep this regression test isolated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/memory/index.spec.ts, line 42:

<comment>A failed assertion leaves `console.warn` mocked, which can suppress diagnostics and affect later tests. Restoring the spy in an `afterEach` hook or a `finally` block would keep this regression test isolated.</comment>

<file context>
@@ -0,0 +1,44 @@
+    );
+    await expect(storage.getConversation("conv-1")).resolves.toBeNull();
+
+    warnSpy.mockRestore();
+  });
+});
</file context>

updated_at: "2024-01-01T00:00:00.000Z",
};

it("adds expectedUserId to updateConversation mutations", async () => {

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.

P3: The new adapter tests do not verify the security-critical failure path: a guarded update or delete must reject when the owner does not match or when the database affects zero rows. Adding negative tests for each adapter, including assertions that the conversation and child data remain unchanged, would protect the revalidation behavior claimed by this change.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cloudflare-d1/src/memory-adapter.spec.ts, line 96:

<comment>The new adapter tests do not verify the security-critical failure path: a guarded update or delete must reject when the owner does not match or when the database affects zero rows. Adding negative tests for each adapter, including assertions that the conversation and child data remain unchanged, would protect the revalidation behavior claimed by this change.</comment>

<file context>
@@ -81,3 +81,59 @@ describe("D1MemoryAdapter queryWorkflowRuns", () => {
+    updated_at: "2024-01-01T00:00:00.000Z",
+  };
+
+  it("adds expectedUserId to updateConversation mutations", async () => {
+    vi.spyOn(D1MemoryAdapter.prototype as any, "ensureInitialized").mockResolvedValue(undefined);
+    const adapter = new D1MemoryAdapter({ binding: createMockBinding(), tablePrefix: "test" });
</file context>

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.

Memory API Missing Ownership Check -- Cross-User Conversation Access (IDOR) in @voltagent/server-hono

2 participants