Skip to content

Agent tools accept arbitrary IDs from LLM with admin privileges — no dataset scoping #68

Description

@simantak-dabhade

Summary

The populate agent's dataset tools (insert_row, update_row, delete_row) run with the Convex admin key but trust IDs provided by the LLM without verifying they belong to the dataset the workflow was authorized for.

Details

The /populate endpoint in backend/src/index.ts correctly checks ownership:

const dataset = await convex.query(api.datasets.get, { id: parsed.data.datasetId });
if (dataset.ownerId !== req.auth.userId) {
  return reply.code(403).send({ error: "Not authorized to populate this dataset" });
}

But once the workflow starts, the agent tools don't enforce this boundary:

  • insert_row (backend/src/mastra/tools/dataset-tools.ts) takes datasetId directly from LLM output and passes it to internal.datasetRows.insert via admin key. Nothing validates it matches the workflow's authorized dataset.
  • update_row takes a rowId and calls internal.datasetRows.update — no check that the row belongs to the authorized dataset.
  • delete_row takes a rowId and calls internal.datasetRows.remove (ctx.db.delete(args.id)) — no dataset scoping at all.

Attack vector

The populate agent ingests web search results into its context (via search_web and fetch_page tools). A malicious website crafted to appear in search results could embed prompt injection payloads that instruct the agent to:

  1. Insert rows into a different user's dataset (consuming their quota)
  2. Update rows in a different dataset
  3. Delete rows from a different dataset

All three succeed because the admin key bypasses all Convex auth checks and the tools don't validate dataset membership.

Suggested fix

Pass the authorized datasetId into the tool execution context (e.g., via Mastra's tool context or closure) and validate inside each tool:

  • insert_row: assert datasetId === authorizedDatasetId
  • update_row: after fetching the row, assert row.datasetId === authorizedDatasetId
  • delete_row: after fetching the row, assert row.datasetId === authorizedDatasetId

This keeps the admin key's power scoped to the dataset the user actually owns.

Affected code

  • backend/src/mastra/tools/dataset-tools.ts (all write tools)
  • frontend/convex/datasetRows.ts (update, remove mutations — no dataset ownership check)

Introduced in #26.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions