GitHub Security Advisory: GHSA-mr6q-rp88-fx84
Package: @astrojs/vercel (NPM)
Severity: MODERATE (CVSS 6.5)
CWE: CWE-441: Unintended Proxy or Intermediary ('Confused Deputy'), CWE-862: Missing Authorization
Vulnerable versions: < 10.0.2
Patched in: 10.0.2

Summary: Astro: Unauthenticated Path Override via `x-astro-path` / `x_astro_path`

Details:
## Summary

The `@astrojs/vercel` serverless entrypoint reads the `x-astro-path` header and `x_astro_path` query parameter to rewrite the internal request path, with no authentication whatsoever. On deployments without Edge Middleware, this lets anyone bypass Vercel's platform-level path restrictions entirely.

The override preserves the original HTTP method and body, so this isn't limited to GET. POST, PUT, DELETE all land on the rewritten path. A Firewall rule blocking `/admin/*` does nothing when the request comes in as `POST /api/health?x_astro_path=/admin/delete-user`.

## Affected Versions

Verified against:
- **Astro 5.18.1 + @astrojs/vercel 9.0.4** — GET and POST override both work. Full exploitation.
- **Astro 6.0.3 + @astrojs/vercel 10.0.0** — GET override works. POST/DELETE hit a `duplex` bug in the Request constructor (the `duplex: 'half'` option is required when passing a ReadableStream body — this has been an issue since Node.js 18 but is consistently enforced in the Node.js 22+ runtime that Astro 6 requires). This is not a security fix — the code explicitly passes `body: request.body` and intends to preserve it. Once the missing `duplex` option is added, all methods will be exploitable on v6 as well.

The vulnerable code path is identical across both versions.

## Affected Component

- **Package**: `@astrojs/vercel`
- **File**: `packages/integrations/vercel/src/serverless/entrypoint.ts` (lines 19–28)
- **Constants**: `packages/integrations/vercel/src/index.ts` (lines 44–45)

## Vulnerable Code

The handler blindly trusts the caller-supplied path:

```typescript
const realPath =
    request.headers.get(ASTRO_PATH_HEADER) ??
    url.searchParams.get(ASTRO_PATH_PARAM);
if (typeof realPath === 'string') {
    url.pathname = realPath;  // no validation, no auth
    request = new Request(url.toString(), {
        method: request.method,   // preserved
        headers: request.headers, // preserved
        body: request.body,       // preserved
    });
}
```

What makes this worse is the inconsistency. `x-astro-locals` right below it is gated behind `middlewareSecret`, but `x-astro-path` gets nothing:

```typescript
// x-astro-locals: protected
if (astroLocalsHeader) {
    if (middlewareSecretHeader !== middlewareSecret) {
        return new Response('Forbidden', { status: 403 });
    }
    locals = JSON.parse(astroLocalsHeader);
}
// x-astro-path: no equivalent check (lines 19-28 above)
```

## Conditions

1. Astro + `@astrojs/vercel` adapter
2. `output: 'server'` (SSR)
3. No `src/middleware.ts` defined, or middleware not using Edge mode

This is a realistic production configuration. Middleware is optional and many deployments skip it.

The `x-astro-path` mechanism exists for a legitimate purpose: when Edge Middleware is present, it forwards requests to a single serverless function (`_render`) and uses this header to communicate the original path. The Edge Middleware always overwrites any client-supplied value with the correct one. But w