VYPR
Moderate severityNVD Advisory· Published Mar 24, 2026· Updated Mar 24, 2026

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

CVE-2026-33768

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.

PackageAffected versionsPatched versions
@astrojs/vercelnpm
< 10.0.210.0.2

Affected products

1

Patches

1
335a204161f5

Require trusted secret for path overrides (#15959)

https://github.com/withastro/astroMatthew PhillipsMar 19, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.