BugForge — 2026.07.15

Vaultly: Next.js Middleware Authorization Bypass (CVE-2025-29927)

BugForge Next.js Middleware Authorization Bypass (CVE-2025-29927) medium

Part 1: Pentest Report

Executive Summary

Vaultly is a team document-vault application built on Next.js 14.2.24 (App Router). Testing found that the platform-level /admin area, described in-app as restricted to “Vaultly HQ staff,” is protected by an authorization check that can be bypassed entirely with a single request header, granting unauthenticated access to the admin console and a live break-glass recovery key.

Testing confirmed 1 finding:

ID Title Severity CVSS CWE Endpoint
F1 Next.js middleware authorization bypass (CVE-2025-29927) Critical 9.1 CWE-285 GET /admin/api/recovery

F1 is a version-pinned instance of CVE-2025-29927. The running build (14.2.24) is one patch below the 14.2.25 release that fixes it. Supplying the internal x-middleware-subrequest header causes the affected Next.js versions to skip middleware, so the authorization gate that protects every /admin route never runs. Because the application enforces that gate only at the middleware layer, the bypass yields unauthenticated access to the full platform-staff surface, demonstrated here by reading the Vaultly HQ break-glass recovery key with no session cookie.


Objective

BugForge medium web lab, solo, 2-hour window. Recover the flag from a Next.js document-vault application, hinted to be “built on a real-life CVE.”


Scope / Initial Access

# Target Application
URL: https://lab-1784005048047-j1plxk.labs-app.bugforge.io/

# Auth details
Registration: POST /api/auth/register (first user of an org becomes "owner")
Session: opaque 32-char cookie vaultly_session (Secure, HttpOnly, SameSite=lax), not a JWT
Org roles: owner, admin, editor, viewer (org-scoped)
Platform area: /admin = "Vaultly HQ staff", separate from org roles
Note: the confirmed exploit requires NO session cookie

A sandbox org was registered during reconnaissance to map the authenticated surface, but the confirmed exploit needs no account and no cookie.


Reconnaissance: Fingerprinting Next.js and Spotting a Middleware-Layer Gate

The application surface was mapped by walking the page and API routes, registering a sandbox org to reach the authenticated views, and pulling the client JavaScript bundles to fingerprint the stack. Two signals from the “built on a real-life CVE” hint shaped the plan: the exact framework version, and which layer produced the /admin denial.

  1. The client JavaScript bundle stamps the framework as Next.js 14.2.24 (window.next={version:"14.2.24",appDir:!0} in 117-*.js), one patch below the 14.2.25 fix for CVE-2025-29927. The stamp was absent from the react-dom chunk and present only in the application chunk.
  2. /dashboard and /settings redirect unauthenticated requests to /login (307), while /admin and its subpaths return a 403 rather than a redirect. This marks /admin as a platform-staff area distinct from the login-gated application, and its role check is separate from the org roles.
  3. The /admin 403 is a 36-byte text/plain body with a bare Vary: Accept-Encoding, unlike every normal route’s text/html response carrying the full Next.js Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch set. The gate response is shaped differently from the app’s route responses, pointing at a check enforced above the route handler.
  4. All /api/* endpoints are POST-only (Allow: OPTIONS, POST). One, /api/files/import, performs a server-side URL fetch, noted as an alternative route to internal endpoints.

Application Architecture

Component Detail
Backend Next.js 14.2.24, App Router, React Server Components (buildId aO8H2P2ktTiQipChrfykZ)
Frontend Server-rendered RSC; data fetching is server-side, no page-specific client chunks
Auth Opaque 32-char session cookie vaultly_session (Secure, HttpOnly, SameSite=lax), not a JWT
Database Not observable from the client

API Surface

Endpoint Method Auth Notes
/api/auth/register POST No First user of an org becomes owner
/api/auth/login POST No Accepts next redirect param
/api/vaults POST Session Returns sequential integer vault id
/api/files/import POST Session Server-side URL fetch (alternative internal-access sink)
/api/members POST Session _action=invite with role owner\|admin\|editor\|viewer
/api/sso POST Session OIDC issuer discovery fetch
/admin, /admin/{users,orgs,staff} GET Platform staff Returns 36-byte text/plain 403 when denied
/admin/api/recovery GET Platform staff Returns break-glass recovery key (F1 target)

Attack Chain Visualization

┌───────────────────────┐     ┌───────────────────────┐     ┌───────────────────────┐     ┌───────────────────────┐
│1. JS bundle version   │     │2. /admin gate         │     │3. Send bypass header  │     │4. 200, no cookie      │
│Next.js 14.2.24        │ ──▶ │36B text/plain 403     │ ──▶ │x-middleware-subrequest│ ──▶ │recovery_key returned  │
│one patch < 14.2.25    │     │shape unlike RSC pages │     │src/middleware x5      │     │objective reached      │
└───────────────────────┘     └───────────────────────┘     └───────────────────────┘     └───────────────────────┘

Findings

F1: Next.js middleware authorization bypass (CVE-2025-29927)

Severity: Critical CVSS v3.1: 9.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N) CWE: CWE-285 (Improper Authorization), CWE-693 (Protection Mechanism Failure) Endpoint: GET /admin/api/recovery Authentication required: No

Description

The application runs Next.js 14.2.24, which is affected by CVE-2025-29927. In affected versions, supplying the internal x-middleware-subrequest request header causes Next.js to skip middleware execution for that request. On this target the /admin authorization gate is enforced at the middleware layer: its denial is a 36-byte text/plain 403 with a stripped Vary header, structurally unlike the text/html RSC response every normal route returns. Skipping middleware removes that gate, so GET /admin/api/recovery reaches its route handler and returns the recovery key with no session cookie present.

The header value must match the compiled middleware module path and repeat it enough to reach Next.js’s recursion limit. Stock payloads (middleware, and middleware repeated five times) returned a byte-identical 403; only src/middleware colon-joined five times fired, which also pins this project to a src/ layout. The joined header has to reach MAX_RECURSION_DEPTH (5 in 14.2.x), so five or more repeats of the correct path trigger the skip while fewer do not. CVE-2025-29927 is fixed in Next.js 14.2.25, one patch above this build.

This finding is scored with the published CVE-2025-29927 vector. The engagement demonstrated the confidentiality disclosure of the recovery key; the bypass removes the middleware gate for every /admin route.

Impact

Unauthenticated access to every route behind the platform authorization gate, including the Vaultly HQ admin console and its break-glass recovery key.

Reproduction

Step 1: Confirm the gate is enforced (baseline)

GET /admin/api/recovery HTTP/1.1
Host: lab-1784005048047-j1plxk.labs-app.bugforge.io
Connection: close

Response: 403 Forbidden, Content-Type: text/plain, Content-Length: 36, body Forbidden — Vaultly HQ staff only. The gate is live and enforced.

Step 2: Bypass to the admin console to discover the break-glass route

GET /admin HTTP/1.1
Host: lab-1784005048047-j1plxk.labs-app.bugforge.io
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
Connection: close

Response: 200 OK, Content-Type: text/html, Content-Length: 8429, the “Vaultly HQ Operations Console” page, which links /admin/api/recovery.

Step 3: Bypass to the recovery route

GET /admin/api/recovery HTTP/1.1
Host: lab-1784005048047-j1plxk.labs-app.bugforge.io
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
Connection: close

Response: 200 OK, Content-Type: application/json, Content-Length: 143:

{"org":"Vaultly HQ","note":"Break-glass recovery key — rotate immediately after use.","recovery_key":"bug{ANaOXbS3rD5UBmD4NcNvkgYlRYbBHobA}"}

The only variable between the 403 and the 200 is the header. The body changed from the 36-byte “Forbidden” text to a 143-byte JSON object containing real privileged data, confirming a genuine bypass rather than a status-only flip.

Remediation

Fix 1: Upgrade Next.js past the patch

// BEFORE (vulnerable)
"next": "14.2.24"

// AFTER (patched: CVE-2025-29927 fixed in 14.2.25)
"next": "14.2.25"

Fix 2: Do not rely on middleware alone for authorization

// BEFORE: /admin authorization enforced only in middleware; the handler trusts it ran
export async function GET(req) {
  return Response.json(await getRecoveryKey())
}

// AFTER: re-check the session and role inside the handler
export async function GET(req) {
  const session = await getSession(req)
  if (!session || !session.isPlatformStaff) {
    return new Response('Forbidden', { status: 403 })
  }
  return Response.json(await getRecoveryKey())
}

Additional recommendations:

  • Strip or reject the x-middleware-subrequest header at the edge or reverse proxy. It is an internal Next.js header and should never arrive from a client.
  • Treat the recovery key as compromised and rotate it. The response note (“rotate immediately after use”) already prescribes this, and the value was disclosed in testing.
  • The untested POST /api/files/import server-side fetch reaches the same internal endpoints over loopback regardless of the Next.js version. Validate and allowlist outbound fetch targets so it cannot be turned toward /admin/api/recovery or other internal routes.

OWASP Top 10 Coverage

  • A01:2021 Broken Access Control: The /admin platform-staff authorization gate is bypassed by a single request header, giving an unauthenticated caller access to admin-only routes and data.
  • A06:2021 Vulnerable and Outdated Components: The application runs Next.js 14.2.24, one patch below the 14.2.25 release that fixes CVE-2025-29927; the finding is a direct consequence of the outdated framework version.

Tools Used

Tool Purpose
curl Sending crafted requests with the bypass header and capturing byte-level before/after pairs
Caido Intercepting proxy for request replay and response inspection
grep Fingerprinting the Next.js version from the JavaScript bundle

References


Part 2: Notes / Knowledge

Key Learnings

  • When a “real-life CVE” hint meets a known framework, grep the app JS bundle for the exact patch version first. A precise version pin turns an open-ended “which of many bugs” search into a single named CVE. Here the hint plus a Next.js App Router stack pointed at the framework, and the bundle carried window.next={version:"14.2.24"}, exactly one patch below the 14.2.25 release that fixes CVE-2025-29927. Deployments and lab authors alike tend to sit on the last vulnerable patch, so treat a version one below a known fix as the primary lead and collapse the vector search onto it. Grep the application or vendor chunk, not only the framework core chunk: the stamp was absent from the react-dom chunk and lived only in the app chunk.

  • A short plaintext 4xx that is structurally unlike the app’s normal responses is returned by a pre-handler layer, not the page. Response shape reveals which layer produced it. On this target /admin answered with a 36-byte text/plain body and a bare Vary: Accept-Encoding, while every normal route returned text/html with the full Next.js Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch set. A raw early-return response never passes through the framework render pipeline, so that shape difference marks the gate as living above the route handler, whether that is middleware, an edge function, or a reverse proxy. Reading it correctly said to prioritize a layer-bypass technique for that specific layer over app-logic or role-based attacks, which is exactly what CVE-2025-29927 is.

  • Confirm an authorization bypass by the response body changing to real privileged data, not just the status flipping to 200. A middleware or gateway bypass skips the outer gate but not necessarily an inner re-check, so a 200 that still renders the denial text is a false win. Diff the body bytes and Content-Length, not only the status code. Here the response went from a 36-byte text/plain “Forbidden” body to a 143-byte application/json object holding a live break-glass recovery key: real sensitive content the gated response never returned, which is what separates a genuine bypass from a re-skinned 403.


Failed Approaches

Approach Result Why It Failed
Stock CVE-2025-29927 payloads (x-middleware-subrequest: middleware, and middleware repeated 5x) Byte-identical 403 Header value must match the compiled middleware path; this project uses a src/ layout, so only src/middleware fired
pages/_middleware variant repeated 5x 403 Wrong module path for an App Router project
Fewer than five repeats of src/middleware (1 through 4) 403 Below the recursion limit; the joined header must reach MAX_RECURSION_DEPTH (5 in 14.2.x) for middleware to be skipped
IDOR on /vaults/{id} for ids 1 through 6 Genuine Next.js 404 (6096-byte RSC not-found page) Our vault was the first created in the instance (id 7); no lower-id cross-tenant vault existed
Mass-assignment to self-promote org role via /api/members Owner could invite role=admin but still got 403 on /admin Org roles are separate from platform HQ-staff authorization; org RBAC is orthogonal to the /admin gate
Session token forgery Not attempted past inspection Opaque 32-char non-JWT cookie with no client-forgeable claim; moot once the header bypass grants cookie-less access

Tags: #webapp #nextjs #access-control #cve-2025-29927 #middleware-bypass #bugforge Document Version: 1.0 Last Updated: 2026-07-15

#access-control #nextjs #middleware-bypass #cve-2025-29927 #bugforge