{"id":"CVE-2026-45718","aliases":["EUVD-2026-32600","GHSA-3263-v5v9-xq8q"],"status":"published","title":"Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows","description":"Budibase is an open-source low-code platform. Prior to 3.38.1, 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. This vulnerability is fixed in 3.38.1.","severity":{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00146,"percentile":0.03872,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-863"],"published":"2026-05-18T17:44:22Z","modified":"2026-10-28T02:30:38Z","fixes":[{"ecosystem":"npm","package":"budibase","fixed_in":["3.38.1"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"budibase","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.38.1"}]}]},{"source":"osv","ecosystem":"npm","package":"budibase","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.38.1"}]}]}],"products":[{"vendor":"budibase","product":"budibase"}],"references":[{"url":"https://github.com/Budibase/budibase/security/advisories/GHSA-3263-v5v9-xq8q","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/Budibase/budibase/releases/tag/3.38.1","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45718","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-3263-v5v9-xq8q"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45718.json","tags":["advisory"]},{"url":"https://github.com/Budibase/budibase","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-45718","title":"Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows","description":"Budibase is an open-source low-code platform. Prior to 3.38.1, 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. This vulnerability is fixed in 3.38.1.","status":"published","published":"2026-05-27T17:07:59.856Z","modified":"2026-05-28T15:08:02.758Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T13:46:43.922571Z"}],"euvd":[{"id":"EUVD-2026-32600","description":"Budibase is an open-source low-code platform. Prior to 3.38.1, 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. This vulnerability is fixed in 3.38.1.","status":"published","published":"2026-05-27T17:07:59Z","modified":"2026-05-28T15:08:02Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"updated_at":"2026-11-02T18:21:02.581093Z"}],"ghsa":[{"id":"GHSA-3263-v5v9-xq8q","title":"Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows","description":"## 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'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.\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 — 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 — 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)` — 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's 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's filter scope\n\ncurl -X POST 'http://localhost:10000/api/tables/\u003cviewId\u003e/actions/\u003cactionId\u003e/trigger' \\\n  -H 'Cookie: budibase:auth=\u003cbasic_user_jwt\u003e' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"rowId\": \"\u003carchived_row_id\u003e\"}'\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's 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's 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's 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's 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```","status":"published","published":"2026-05-18T17:44:22Z","modified":"2026-06-08T23:51:46Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T14:40:21.48274Z"}],"nvd":[{"id":"CVE-2026-45718","description":"Budibase is an open-source low-code platform. Prior to 3.38.1, 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. This vulnerability is fixed in 3.38.1.","status":"published","published":"2026-05-27T18:16:25.873Z","modified":"2026-06-17T10:52:29.947Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T14:12:20.885865Z"}],"osv":[{"id":"CVE-2026-45718","title":"Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows","description":"Budibase is an open-source low-code platform. Prior to 3.38.1, 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. This vulnerability is fixed in 3.38.1.","status":"published","published":"2026-05-27T17:07:59.856Z","modified":"2026-10-28T02:30:38.694171654Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T15:45:05.525666Z"},{"id":"GHSA-3263-v5v9-xq8q","title":"Budibase: Row Action Trigger Bypasses View Row Filter Security Boundary Allowing Action on Out-of-Scope Rows","description":"## 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'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.\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 — 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 — 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)` — 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's 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's filter scope\n\ncurl -X POST 'http://localhost:10000/api/tables/\u003cviewId\u003e/actions/\u003cactionId\u003e/trigger' \\\n  -H 'Cookie: budibase:auth=\u003cbasic_user_jwt\u003e' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"rowId\": \"\u003carchived_row_id\u003e\"}'\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's 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's 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's 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's 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```","status":"published","published":"2026-05-18T17:44:22Z","modified":"2026-06-09T00:00:21.945874162Z","cvss":[{"version":"3.1","score":5.4,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N","level":"medium"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T15:46:35.050232Z"}]}}
