Improper Control of Dynamically-Managed Code Resources in budibase/budibase
Description
CVE-2022-3225 is an improper control of dynamically-managed code resources vulnerability in Budibase prior to 1.3.20, allowing potential code injection via query string manipulation.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2022-3225 is an improper control of dynamically-managed code resources vulnerability in Budibase prior to 1.3.20, allowing potential code injection via query string manipulation.
Vulnerability
Overview CVE-2022-3225 is an improper control of dynamically-managed code resources vulnerability in the Budibase open-source operations platform, affecting all versions prior to 1.3.20. The flaw involves inadequate encoding or validation of query string parameters in REST datasource queries, which could allow an attacker to inject malicious code that gets executed in the context of the application [1][2]. The fix in commit d35864be addresses this by introducing decodeURIComponent and findHBSBlocks for proper string handling, preventing injection of Handlebars templates or other code constructs [2].
Exploitation
To exploit this vulnerability, an attacker would need access to create or modify REST datasource queries within Budibase. The attack vector involves crafting a malicious query string that, when processed by the application, bypasses security controls and executes arbitrary code. The vulnerability is classified with a CVSS v3.1 score of 8.8 (High) per NVD metrics, indicating network-based exploitation with low complexity and no privileges required [1]. However, successful exploitation may require some level of authenticated access to create datasource configurations, though exact privileges are not fully detailed in public sources.
Impact
If successfully exploited, an attacker could achieve remote code execution within the Budibase server environment. This could lead to complete compromise of the application, including data exfiltration, modification of databases, and potential lateral movement to connected systems. Given Budibase's role as an operations platform handling business data and automations [3], the impact could extend to all integrated AI agents, workflows, and apps managed through the platform.
Mitigation
Budibase released version 1.3.20 on August 24, 2022, which patches this vulnerability by properly encoding query strings and sanitizing inputs [4]. Users are strongly advised to upgrade to this version or later. As of the publication date, this CVE is not listed in CISA's Known Exploited Vulnerabilities catalog. Organizations using Budibase should prioritize updating their instances, especially those exposed to untrusted users or the internet.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@budibase/workernpm | < 1.3.20 | 1.3.20 |
@budibase/buildernpm | < 1.3.20 | 1.3.20 |
@budibase/bbuinpm | < 1.3.20 | 1.3.20 |
Affected products
4- ghsa-coords3 versions
< 1.3.20+ 2 more
- (no CPE)range: < 1.3.20
- (no CPE)range: < 1.3.20
- (no CPE)range: < 1.3.20
Patches
1d35864be0854Fixing issue introduced by fix for #7683 - encoding the query string caused handlebars statements to break, this rectifies that.
6 files changed · +39 −31
packages/bbui/src/Tooltip/TooltipWrapper.svelte+1 −1 modified@@ -47,7 +47,7 @@ display: flex; justify-content: center; top: 15px; - z-index: 100; + z-index: 200; width: 160px; } .icon {
packages/builder/src/builderStore/dataBinding.js+5 −7 modified@@ -9,14 +9,14 @@ import { import { store } from "builderStore" import { queries as queriesStores, - tables as tablesStore, roles as rolesStore, + tables as tablesStore, } from "stores/backend" import { - makePropSafe, - isJSBinding, decodeJSBinding, encodeJSBinding, + isJSBinding, + makePropSafe, } from "@budibase/string-templates" import { TableNames } from "../constants" import { JSONUtils } from "@budibase/frontend-core" @@ -118,8 +118,7 @@ export const readableToRuntimeMap = (bindings, ctx) => { return {} } return Object.keys(ctx).reduce((acc, key) => { - let parsedQuery = readableToRuntimeBinding(bindings, ctx[key]) - acc[key] = parsedQuery + acc[key] = readableToRuntimeBinding(bindings, ctx[key]) return acc }, {}) } @@ -132,8 +131,7 @@ export const runtimeToReadableMap = (bindings, ctx) => { return {} } return Object.keys(ctx).reduce((acc, key) => { - let parsedQuery = runtimeToReadableBinding(bindings, ctx[key]) - acc[key] = parsedQuery + acc[key] = runtimeToReadableBinding(bindings, ctx[key]) return acc }, {}) }
packages/builder/src/helpers/data/utils.js+16 −2 modified@@ -1,4 +1,5 @@ import { IntegrationTypes } from "constants/backend" +import { findHBSBlocks } from "@budibase/string-templates" export function schemaToFields(schema) { const response = {} @@ -31,7 +32,8 @@ export function breakQueryString(qs) { let paramObj = {} for (let param of params) { const split = param.split("=") - paramObj[split[0]] = split.slice(1).join("=") + console.log(split[1]) + paramObj[split[0]] = decodeURIComponent(split.slice(1).join("=")) } return paramObj } @@ -46,7 +48,19 @@ export function buildQueryString(obj) { if (str !== "") { str += "&" } - str += `${key}=${encodeURIComponent(value || "")}` + const bindings = findHBSBlocks(value) + let count = 0 + const bindingMarkers = {} + bindings.forEach(binding => { + const marker = `BINDING...${count++}` + value = value.replace(binding, marker) + bindingMarkers[marker] = binding + }) + let encoded = encodeURIComponent(value || "") + Object.entries(bindingMarkers).forEach(([marker, binding]) => { + encoded = encoded.replace(marker, binding) + }) + str += `${key}=${encoded}` } } return str
packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/rest/[query]/index.svelte+2 −0 modified@@ -347,6 +347,7 @@ const datasourceUrl = datasource?.config.url const qs = query?.fields.queryString breakQs = restUtils.breakQueryString(qs) + console.log(breakQs) breakQs = runtimeToReadableMap(mergedBindings, breakQs) const path = query.fields.path @@ -708,6 +709,7 @@ .url-block { display: flex; gap: var(--spacing-s); + z-index: 200; } .verb { flex: 1;
packages/worker/src/api/controllers/global/self.js+14 −17 modified@@ -80,16 +80,15 @@ const addSessionAttributesToUser = ctx => { ctx.body.csrfToken = ctx.user.csrfToken } -/** - * Remove the attributes that are session based from the current user, - * so that stale values are not written to the db - */ -const removeSessionAttributesFromUser = ctx => { - delete ctx.request.body.csrfToken - delete ctx.request.body.account - delete ctx.request.body.accountPortalAccess - delete ctx.request.body.budibaseAccess - delete ctx.request.body.license +const sanitiseUserUpdate = ctx => { + const allowed = ["firstName", "lastName", "password", "forceResetPassword"] + const resp = {} + for (let [key, value] of Object.entries(ctx.request.body)) { + if (allowed.includes(key)) { + resp[key] = value + } + } + return resp } exports.getSelf = async ctx => { @@ -117,25 +116,23 @@ exports.updateSelf = async ctx => { const db = getGlobalDB() const user = await db.get(ctx.user._id) let passwordChange = false - if (ctx.request.body.password) { + + const userUpdateObj = sanitiseUserUpdate(ctx) + if (userUpdateObj.password) { // changing password passwordChange = true - ctx.request.body.password = await hash(ctx.request.body.password) + userUpdateObj.password = await hash(userUpdateObj.password) // Log all other sessions out apart from the current one await platformLogout({ ctx, userId: ctx.user._id, keepActiveSession: true, }) } - // don't allow sending up an ID/Rev, always use the existing one - delete ctx.request.body._id - delete ctx.request.body._rev - removeSessionAttributesFromUser(ctx) const response = await db.put({ ...user, - ...ctx.request.body, + ...userUpdateObj, }) await userCache.invalidateUser(user._id) ctx.body = {
packages/worker/src/api/controllers/global/users.ts+1 −4 modified@@ -14,7 +14,6 @@ import { errors, events, tenancy, - users as usersCore, } from "@budibase/backend-core" import { checkAnyUserExists } from "../../../utilities/users" import { groups as groupUtils } from "@budibase/pro" @@ -148,9 +147,7 @@ export const bulkDelete = async (ctx: any) => { } try { - let response = await users.bulkDelete(userIds) - - ctx.body = response + ctx.body = await users.bulkDelete(userIds) } catch (err) { ctx.throw(err) }
Vulnerability mechanics
Root cause
"Missing input allowlist in user profile update allows arbitrary properties to be written to the database."
Attack vector
An authenticated attacker can send a crafted HTTP request to the `updateSelf` endpoint with arbitrary JSON properties in the request body (e.g., `role`, `admin`, `permissions`). Because the original code only deleted a handful of known session attributes and the `_id`/`_rev` fields, any other property was spread into the user document via `...ctx.request.body` and persisted to the database [CWE-284]. This allows a low-privileged user to escalate privileges or modify sensitive account fields by including them in the update payload [CWE-913].
Affected code
The primary vulnerable code path is in `packages/worker/src/api/controllers/global/self.js`, where the `updateSelf` endpoint previously deleted only a few session attributes (`csrfToken`, `account`, `accountPortalAccess`, `budibaseAccess`, `license`) and the `_id`/`_rev` fields from `ctx.request.body`, but did not restrict arbitrary user-controlled properties from being written to the database. The patch replaces this fragile blocklist approach with a strict allowlist (`sanitiseUserUpdate`) that only permits `firstName`, `lastName`, `password`, and `forceResetPassword` [patch_id=1705202].
What the fix does
The patch replaces the `removeSessionAttributesFromUser` function (which deleted only a fixed set of session-based keys) with a new `sanitiseUserUpdate` function that builds a response object containing only keys from an explicit allowlist: `["firstName", "lastName", "password", "forceResetPassword"]` [patch_id=1705202]. Instead of spreading `ctx.request.body` directly into the user document, the code now spreads `userUpdateObj`, ensuring no arbitrary properties (such as role or permission fields) can be injected. This closes the improper access control by enforcing a strict allowlist rather than a fragile blocklist.
Preconditions
- authAttacker must have a valid authenticated session with the Budibase application
- networkAttacker must be able to send HTTP requests to the /api/self endpoint
- configThe vulnerable version must be prior to 1.3.20
Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-x92g-49gh-63qmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-3225ghsaADVISORY
- github.com/Budibase/budibase/releases/tag/v1.3.20ghsaWEB
- github.com/budibase/budibase/commit/d35864be0854216693a01307f81ffcabf6d549dfghsax_refsource_MISCWEB
- huntr.dev/bounties/a13a56b7-04da-4560-b8ec-0d637d12a245ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.