Astro: Unauthenticated Path Override via `x-astro-path` / `x_astro_path`
Description
Astro is a web framework. Prior to version 10.0.2, 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. This issue has been patched in version 10.0.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@astrojs/vercelnpm | < 10.0.2 | 10.0.2 |
Affected products
1Patches
1335a204161f5Require trusted secret for path overrides (#15959)
3 files changed · +67 −7
.changeset/short-cycles-fail.md+5 −0 added@@ -0,0 +1,5 @@ +--- +'@astrojs/vercel': patch +--- + +Fix Vercel serverless path override handling so override values are only applied when the trusted middleware secret is present.
packages/integrations/vercel/src/serverless/entrypoint.ts+9 −7 modified@@ -3,7 +3,6 @@ import { ASTRO_LOCALS_HEADER, ASTRO_MIDDLEWARE_SECRET_HEADER, ASTRO_PATH_HEADER, - ASTRO_PATH_PARAM, } from '../index.js'; import { middlewareSecret, skewProtection } from 'virtual:astro-vercel:config'; import { createApp } from 'astro/app/entrypoint'; @@ -16,8 +15,9 @@ const app = createApp(); export default { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); - const realPath = - request.headers.get(ASTRO_PATH_HEADER) ?? url.searchParams.get(ASTRO_PATH_PARAM); + const middlewareSecretHeader = request.headers.get(ASTRO_MIDDLEWARE_SECRET_HEADER); + const hasValidMiddlewareSecret = middlewareSecretHeader === middlewareSecret; + const realPath = hasValidMiddlewareSecret ? request.headers.get(ASTRO_PATH_HEADER) : null; if (typeof realPath === 'string') { url.pathname = realPath; request = new Request(url.toString(), { @@ -32,16 +32,18 @@ export default { let locals: Record<string, unknown> = {}; const astroLocalsHeader = request.headers.get(ASTRO_LOCALS_HEADER); - const middlewareSecretHeader = request.headers.get(ASTRO_MIDDLEWARE_SECRET_HEADER); if (astroLocalsHeader) { - if (middlewareSecretHeader !== middlewareSecret) { + if (!hasValidMiddlewareSecret) { return new Response('Forbidden', { status: 403 }); } - // hide the secret from the rest of user code - request.headers.delete(ASTRO_MIDDLEWARE_SECRET_HEADER); locals = JSON.parse(astroLocalsHeader); } + // hide the secret from the rest of user code + if (hasValidMiddlewareSecret) { + request.headers.delete(ASTRO_MIDDLEWARE_SECRET_HEADER); + } + // https://vercel.com/docs/deployments/skew-protection#supported-frameworks if (skewProtection && process.env.VERCEL_SKEW_PROTECTION_ENABLED === '1') { request.headers.set('x-deployment-id', process.env.VERCEL_DEPLOYMENT_ID!);
packages/integrations/vercel/test/path-override-security.test.js+53 −0 added@@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { loadFixture } from './test-utils.js'; + +async function loadFunctionModule(fixture, functionName) { + const functionConfig = JSON.parse( + await fixture.readFile(`../.vercel/output/functions/${functionName}.func/.vc-config.json`), + ); + const functionEntry = new URL( + `../.vercel/output/functions/${functionName}.func/${functionConfig.handler}`, + fixture.config.outDir, + ); + + return import(functionEntry); +} + +describe('Vercel serverless path override security', () => { + /** @type {import('./test-utils.js').Fixture} */ + let fixture; + + before(async () => { + process.env.PRERENDER = true; + fixture = await loadFixture({ + root: './fixtures/serverless-with-dynamic-routes/', + output: 'server', + }); + await fixture.build(); + }); + + it('ignores untrusted x_astro_path query param on _render', async () => { + const renderFunction = await loadFunctionModule(fixture, '_render'); + const response = await renderFunction.default.fetch( + new Request('https://example.com/api/public?x_astro_path=/api/private'), + ); + const body = await response.json(); + + assert.equal(body.id, 'public'); + }); + + it('ignores untrusted x-astro-path header on _render', async () => { + const renderFunction = await loadFunctionModule(fixture, '_render'); + const response = await renderFunction.default.fetch( + new Request('https://example.com/api/public', { + headers: { + 'x-astro-path': '/api/private', + }, + }), + ); + const body = await response.json(); + + assert.equal(body.id, 'public'); + }); +});
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-f82v-jwr5-mffwghsaADVISORY
- github.com/advisories/GHSA-mr6q-rp88-fx84ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33768ghsaADVISORY
- github.com/withastro/astro/commit/335a204161f5a7293c128db570901d4f8639c6edghsax_refsource_MISCWEB
- github.com/withastro/astro/pull/15959ghsax_refsource_MISCWEB
- github.com/withastro/astro/releases/tag/%40astrojs%2Fvercel%4010.0.2mitrex_refsource_MISC
- github.com/withastro/astro/releases/tag/@astrojs/vercel@10.0.2ghsaWEB
- github.com/withastro/astro/security/advisories/GHSA-mr6q-rp88-fx84ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.