GHSA-3263-V5V9-XQ8Q

Vulnerability from github – Published: 2026-05-18 17:44 – Updated: 2026-05-18 17:44
VLAI
Summary
Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows
Details

Summary

The row action trigger endpoint (POST /api/tables/:sourceId/actions/:actionId/trigger) fails to validate that the user-supplied rowId is within the scope of the view's row filters. A user with access to a filtered view can trigger row actions on any row in the underlying table, including rows explicitly excluded by the view's security filters.

Details

View filters in Budibase are treated as a security boundary. The search path (packages/server/src/sdk/workspace/rows/search.ts:93-94) explicitly enforces view query filters with the comment: "that could let users find rows they should not be allowed to access."

However, the row action trigger path bypasses this enforcement entirely:

  1. Route (packages/server/src/api/routes/rowAction.ts:55-59): Accepts a sourceId that can be a viewId.

  2. Middleware (packages/server/src/middleware/triggerRowActionAuthorised.ts:24-55): Correctly validates that the user has READ permission on the view and that the row action is enabled for that view. However, at line 55 it sets ctx.params.tableId = tableId where tableId is the underlying table extracted from the viewId — the viewId is discarded.

// triggerRowActionAuthorised.ts:24-26
const tableId = isTableIdOrExternalTableId(sourceId)
  ? sourceId
  : getTableIdFromViewId(sourceId)  // extracts underlying table

// Line 55: viewId context is lost
ctx.params.tableId = tableId
  1. Controller (packages/server/src/api/controllers/rowAction/run.ts:11): Reads only tableId from params — the view context is gone.
const { tableId, actionId } = ctx.params
const { rowId } = ctx.request.body
await sdk.rowActions.run(tableId, actionId, rowId, ctx.user)
  1. SDK (packages/server/src/sdk/workspace/rowActions/crud.ts:254): Fetches the row using sdk.rows.find(tableId, rowId) — directly from the table with no view filter enforcement.
const row = await sdk.rows.find(tableId, rowId)  // No view filter check

The sdk.rows.find function (packages/server/src/sdk/workspace/rows/internal.ts:67-88) fetches the row by ID directly from the database, only validating that row.tableId === tableId. It never checks whether the row matches the view's query filters.

PoC

# Prerequisites:
# 1. Create a table with a "status" column containing rows: "active" and "archived"
# 2. Create a view filtering to status="active", assign it to BASIC role
# 3. Enable a row action for that view
# 4. Note the rowId of an "archived" row (not visible through the view)

# As a BASIC-role user with access only to the filtered view:
# Trigger the row action on a row OUTSIDE the view's filter scope

curl -X POST 'http://localhost:10000/api/tables/<viewId>/actions/<actionId>/trigger' \
  -H 'Cookie: budibase:auth=<basic_user_jwt>' \
  -H 'Content-Type: application/json' \
  -d '{"rowId": "<archived_row_id>"}'

# Expected: 403 or 404 (row not in view scope)
# Actual: 200 {"message": "Row action triggered."}
# The automation executes with the full archived row data,
# despite view filters excluding it from the user's access.

Impact

A user with BASIC role access to a filtered view can execute row actions (automations) on any row in the underlying table, including rows hidden by the view's security filters. The impact depends on what the triggered automation does:

  • Information disclosure: The automation receives the full row data as input, which may contain fields/values the user should not see.
  • Unauthorized data modification: If the automation modifies rows, the attacker can cause changes to rows outside their authorized scope.
  • Unauthorized actions: If the automation sends notifications, calls webhooks, or performs other side effects, the attacker can trigger these for out-of-scope rows.

This breaks the security model established by view filters, which are explicitly documented as preventing users from accessing rows they should not see.

Recommended Fix

The middleware should pass the viewId to the controller, and the SDK run function should validate the row against the view's filters before executing the automation.

In packages/server/src/middleware/triggerRowActionAuthorised.ts, preserve the sourceId:

// Line 55: preserve the original sourceId for downstream filter validation
ctx.params.tableId = tableId
ctx.params.sourceId = viewId || tableId  // ADD THIS

In packages/server/src/api/controllers/rowAction/run.ts, pass the sourceId:

export async function run(
  ctx: Ctx<RowActionTriggerRequest, RowActionTriggerResponse>
) {
  const { tableId, actionId, sourceId } = ctx.params
  const { rowId } = ctx.request.body

  await sdk.rowActions.run(tableId, actionId, rowId, ctx.user, sourceId)
  ctx.body = { message: "Row action triggered." }
}

In packages/server/src/sdk/workspace/rowActions/crud.ts, validate the row against view filters:

export async function run(
  tableId: any,
  rowActionId: any,
  rowId: string,
  user: User,
  sourceId?: string
) {
  const table = await sdk.tables.getTable(tableId)
  if (!table) {
    throw new HTTPError("Table not found", 404)
  }

  // If triggered from a view, validate the row is within the view's scope
  if (sourceId && isViewId(sourceId)) {
    const result = await sdk.rows.search({
      viewId: sourceId,
      query: { equal: { _id: rowId } },
      limit: 1,
    })
    if (!result.rows.length) {
      throw new HTTPError("Row not found in view scope", 403)
    }
  }

  const { automationId } = await get(tableId, rowActionId)
  const automation = await sdk.automations.get(automationId)
  const row = await sdk.rows.find(tableId, rowId)
  // ... rest unchanged
}
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "budibase"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.38.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-45718"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-18T17:44:22Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nThe row action trigger endpoint (`POST /api/tables/:sourceId/actions/:actionId/trigger`) fails to validate that the user-supplied `rowId` is within the scope of the view\u0027s row filters. A user with access to a filtered view can trigger row actions on any row in the underlying table, including rows explicitly excluded by the view\u0027s security filters.\n\n## Details\n\nView filters in Budibase are treated as a security boundary. The search path (`packages/server/src/sdk/workspace/rows/search.ts:93-94`) explicitly enforces view query filters with the comment: *\"that could let users find rows they should not be allowed to access.\"*\n\nHowever, the row action trigger path bypasses this enforcement entirely:\n\n1. **Route** (`packages/server/src/api/routes/rowAction.ts:55-59`): Accepts a `sourceId` that can be a viewId.\n\n2. **Middleware** (`packages/server/src/middleware/triggerRowActionAuthorised.ts:24-55`): Correctly validates that the user has READ permission on the view and that the row action is enabled for that view. However, at line 55 it sets `ctx.params.tableId = tableId` where `tableId` is the **underlying table** extracted from the viewId \u2014 the viewId is discarded.\n\n```typescript\n// triggerRowActionAuthorised.ts:24-26\nconst tableId = isTableIdOrExternalTableId(sourceId)\n  ? sourceId\n  : getTableIdFromViewId(sourceId)  // extracts underlying table\n\n// Line 55: viewId context is lost\nctx.params.tableId = tableId\n```\n\n3. **Controller** (`packages/server/src/api/controllers/rowAction/run.ts:11`): Reads only `tableId` from params \u2014 the view context is gone.\n\n```typescript\nconst { tableId, actionId } = ctx.params\nconst { rowId } = ctx.request.body\nawait sdk.rowActions.run(tableId, actionId, rowId, ctx.user)\n```\n\n4. **SDK** (`packages/server/src/sdk/workspace/rowActions/crud.ts:254`): Fetches the row using `sdk.rows.find(tableId, rowId)` \u2014 directly from the table with no view filter enforcement.\n\n```typescript\nconst row = await sdk.rows.find(tableId, rowId)  // No view filter check\n```\n\nThe `sdk.rows.find` function (`packages/server/src/sdk/workspace/rows/internal.ts:67-88`) fetches the row by ID directly from the database, only validating that `row.tableId === tableId`. It never checks whether the row matches the view\u0027s query filters.\n\n## PoC\n\n```bash\n# Prerequisites:\n# 1. Create a table with a \"status\" column containing rows: \"active\" and \"archived\"\n# 2. Create a view filtering to status=\"active\", assign it to BASIC role\n# 3. Enable a row action for that view\n# 4. Note the rowId of an \"archived\" row (not visible through the view)\n\n# As a BASIC-role user with access only to the filtered view:\n# Trigger the row action on a row OUTSIDE the view\u0027s filter scope\n\ncurl -X POST \u0027http://localhost:10000/api/tables/\u003cviewId\u003e/actions/\u003cactionId\u003e/trigger\u0027 \\\n  -H \u0027Cookie: budibase:auth=\u003cbasic_user_jwt\u003e\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"rowId\": \"\u003carchived_row_id\u003e\"}\u0027\n\n# Expected: 403 or 404 (row not in view scope)\n# Actual: 200 {\"message\": \"Row action triggered.\"}\n# The automation executes with the full archived row data,\n# despite view filters excluding it from the user\u0027s access.\n```\n\n## Impact\n\nA user with BASIC role access to a filtered view can execute row actions (automations) on **any row** in the underlying table, including rows hidden by the view\u0027s security filters. The impact depends on what the triggered automation does:\n\n- **Information disclosure**: The automation receives the full row data as input, which may contain fields/values the user should not see.\n- **Unauthorized data modification**: If the automation modifies rows, the attacker can cause changes to rows outside their authorized scope.\n- **Unauthorized actions**: If the automation sends notifications, calls webhooks, or performs other side effects, the attacker can trigger these for out-of-scope rows.\n\nThis breaks the security model established by view filters, which are explicitly documented as preventing users from accessing rows they should not see.\n\n## Recommended Fix\n\nThe middleware should pass the `viewId` to the controller, and the SDK `run` function should validate the row against the view\u0027s filters before executing the automation.\n\nIn `packages/server/src/middleware/triggerRowActionAuthorised.ts`, preserve the sourceId:\n\n```typescript\n// Line 55: preserve the original sourceId for downstream filter validation\nctx.params.tableId = tableId\nctx.params.sourceId = viewId || tableId  // ADD THIS\n```\n\nIn `packages/server/src/api/controllers/rowAction/run.ts`, pass the sourceId:\n\n```typescript\nexport async function run(\n  ctx: Ctx\u003cRowActionTriggerRequest, RowActionTriggerResponse\u003e\n) {\n  const { tableId, actionId, sourceId } = ctx.params\n  const { rowId } = ctx.request.body\n\n  await sdk.rowActions.run(tableId, actionId, rowId, ctx.user, sourceId)\n  ctx.body = { message: \"Row action triggered.\" }\n}\n```\n\nIn `packages/server/src/sdk/workspace/rowActions/crud.ts`, validate the row against view filters:\n\n```typescript\nexport async function run(\n  tableId: any,\n  rowActionId: any,\n  rowId: string,\n  user: User,\n  sourceId?: string\n) {\n  const table = await sdk.tables.getTable(tableId)\n  if (!table) {\n    throw new HTTPError(\"Table not found\", 404)\n  }\n\n  // If triggered from a view, validate the row is within the view\u0027s scope\n  if (sourceId \u0026\u0026 isViewId(sourceId)) {\n    const result = await sdk.rows.search({\n      viewId: sourceId,\n      query: { equal: { _id: rowId } },\n      limit: 1,\n    })\n    if (!result.rows.length) {\n      throw new HTTPError(\"Row not found in view scope\", 403)\n    }\n  }\n\n  const { automationId } = await get(tableId, rowActionId)\n  const automation = await sdk.automations.get(automationId)\n  const row = await sdk.rows.find(tableId, rowId)\n  // ... rest unchanged\n}\n```",
  "id": "GHSA-3263-v5v9-xq8q",
  "modified": "2026-05-18T17:44:22Z",
  "published": "2026-05-18T17:44:22Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/security/advisories/GHSA-3263-v5v9-xq8q"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Budibase/budibase"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Budibase/budibase/releases/tag/3.38.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…