Severity by source
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Lifecycle Timeline
2DescriptionGitHub Advisory
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:
- Route (
packages/server/src/api/routes/rowAction.ts:55-59): Accepts asourceIdthat can be a viewId. - 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 setsctx.params.tableId = tableIdwheretableIdis 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- Controller (
packages/server/src/api/controllers/rowAction/run.ts:11): Reads onlytableIdfrom 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)- SDK (
packages/server/src/sdk/workspace/rowActions/crud.ts:254): Fetches the row usingsdk.rows.find(tableId, rowId)- directly from the table with no view filter enforcement.
const row = await sdk.rows.find(tableId, rowId) // No view filter checkThe 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 THISIn 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
}AnalysisAI
Row action trigger endpoint in Budibase allows authenticated low-privilege users to execute automations on rows outside their authorized view scope, bypassing a documented security boundary. Any user holding BASIC-role READ access to a filtered view can supply an arbitrary rowId to POST /api/tables/:sourceId/actions/:actionId/trigger and invoke automations against rows explicitly excluded by the view's filters. Publicly available exploit code (curl PoC) is included in the GHSA advisory; this vulnerability is not listed in CISA KEV and no confirmed widespread active exploitation has been identified at time of analysis.
Technical ContextAI
Budibase is a low-code application platform distributed as an npm package (pkg:npm/budibase). View filters are documented and implemented as a security boundary in packages/server/src/sdk/workspace/rows/search.ts:93-94, which enforces query filters to prevent users from accessing rows outside their permitted scope. The vulnerability (CWE-863: Incorrect Authorization) arises because the row action trigger pipeline discards the viewId context partway through request processing: the middleware triggerRowActionAuthorised.ts:55 replaces ctx.params with the underlying tableId extracted from the viewId, so the downstream controller (rowAction/run.ts) and SDK (rowActions/crud.ts:254) operate on the raw table. The sdk.rows.find function (rows/internal.ts:67-88) then fetches rows directly from the database, validating only that row.tableId === tableId - never consulting the view's query filters. The authorization check in the middleware is structurally correct for permission validation but architecturally loses the view scope before it can be applied to the row-level access decision.
RemediationAI
Vendor-released patch: 3.38.1. Upgrade Budibase to version 3.38.1 or later, which includes the targeted security fix '[Security] Enforce view scope for row action triggers' (PR #18754), confirmed in the official GitHub release at https://github.com/Budibase/budibase/releases/tag/3.38.1. If immediate upgrade is not feasible, the most actionable compensating control is to disable row actions on any view that functions as a security boundary for sensitive or restricted data - this removes the vulnerable code path entirely at the cost of losing that automation functionality. A secondary option is to restrict the POST /api/tables/*/actions/*/trigger endpoint at a reverse proxy or API gateway to internal or trusted networks only, reducing exposure to external attackers; note this does not address exploitation by already-authenticated internal users. No generic network filtering eliminates the flaw - the root cause is an authorization bypass in application logic, not a network exposure issue.
Same weakness CWE-863 – Incorrect Authorization
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-32600
GHSA-3263-v5v9-xq8q