VYPR
Moderate severityNVD Advisory· Published Dec 8, 2025· Updated Dec 9, 2025

Astro has an Authentication Bypass via Double URL Encoding, a bypass for CVE-2025-64765

CVE-2025-66202

Description

Astro is a web framework. Versions 5.15.7 and below have a double URL encoding bypass which allows any unauthenticated attacker to bypass path-based authentication checks in Astro middleware, granting unauthorized access to protected routes. While the original CVE-2025-64765 was fixed in v5.15.8, the fix is insufficient as it only decodes once. By using double-encoded URLs, attackers can still bypass authentication and access any route protected by middleware pathname checks. This issue is fixed in version 5.15.8.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
astronpm
< 5.15.85.15.8

Affected products

1

Patches

1
6f800813516b

Fix middleware pathname matching by normalizing URL-encoded paths (#14771)

https://github.com/withastro/astroMatthew PhillipsNov 15, 2025via ghsa
7 files changed · +88 6
  • .changeset/middleware-path-normalization.md+7 0 added
    @@ -0,0 +1,7 @@
    +---
    +"astro": "patch"
    +---
    +
    +Fix middleware pathname matching by normalizing URL-encoded paths
    +
    +Middleware now receives normalized pathname values, ensuring that encoded paths like `/%61dmin` are properly decoded to `/admin` before middleware checks. This prevents potential security issues where middleware checks might be bypassed through URL encoding.
    
  • packages/astro/src/core/render-context.ts+12 3 modified
    @@ -61,7 +61,7 @@ export class RenderContext {
     		public clientAddress: string | undefined,
     		protected cookies = new AstroCookies(request),
     		public params = getParams(routeData, pathname),
    -		protected url = new URL(request.url),
    +		protected url = RenderContext.#createNormalizedUrl(request.url),
     		public props: Props = {},
     		public partial: undefined | boolean = undefined,
     		public shouldInjectCspMetaTags = !!pipeline.manifest.csp,
    @@ -70,6 +70,15 @@ export class RenderContext {
     			: undefined,
     	) {}
     
    +	static #createNormalizedUrl(requestUrl: string): URL {
    +		const url = new URL(requestUrl);
    +		try {
    +			url.pathname = decodeURI(url.pathname);
    +		} finally {
    +			return url;
    +		}
    +	}
    +
     	/**
     	 * A flag that tells the render content if the rewriting was triggered
     	 */
    @@ -217,7 +226,7 @@ export class RenderContext {
     					);
     				}
     				this.isRewriting = true;
    -				this.url = new URL(this.request.url);
    +				this.url = RenderContext.#createNormalizedUrl(this.request.url);
     				this.params = getParams(routeData, pathname);
     				this.pathname = pathname;
     				this.status = 200;
    @@ -362,7 +371,7 @@ export class RenderContext {
     				this.routeData.route,
     			);
     		}
    -		this.url = new URL(this.request.url);
    +		this.url = RenderContext.#createNormalizedUrl(this.request.url);
     		const newCookies = new AstroCookies(this.request);
     		if (this.cookies) {
     			newCookies.merge(this.cookies);
    
  • packages/astro/src/vite-plugin-astro-server/request.ts+1 1 modified
    @@ -41,7 +41,7 @@ export async function handleRequest({
     	}
     
     	// Add config.base back to url before passing it to SSR
    -	url.pathname = removeTrailingForwardSlash(config.base) + url.pathname;
    +	url.pathname = removeTrailingForwardSlash(config.base) + decodeURI(url.pathname);
     
     	// Apply trailing slash configuration consistently
     	if (config.trailingSlash === 'never') {
    
  • packages/astro/test/actions.test.js+2 2 modified
    @@ -117,7 +117,7 @@ describe('Astro Actions', () => {
     		});
     
     		it('Handles special characters in action names', async () => {
    -			for (const name of ['with%2Fslash', 'with%20space', 'with%2Edot']) {
    +			for (const name of ['with%2Fslash', 'with%20space']) {
     				const res = await fixture.fetch(`/_actions/${name}`, {
     					method: 'POST',
     					body: JSON.stringify({ name: 'ben' }),
    @@ -534,7 +534,7 @@ describe('Astro Actions', () => {
     		});
     
     		it('Handles special characters in action names', async () => {
    -			for (const name of ['with%2Fslash', 'with%20space', 'with%2Edot']) {
    +			for (const name of ['with%2Fslash', 'with%20space']) {
     				const req = new Request(`http://example.com/_actions/${name}`, {
     					method: 'POST',
     					body: JSON.stringify({ name: 'ben' }),
    
  • packages/astro/test/fixtures/middleware space/src/middleware.js+10 0 modified
    @@ -1,6 +1,16 @@
     import { defineMiddleware, sequence } from 'astro:middleware';
     
     const first = defineMiddleware(async (context, next) => {
    +	// Auth check: protect /admin route
    +	if (context.url.pathname === '/admin') {
    +		const authToken = context.request.headers.get('Authorization');
    +		if (!authToken) {
    +			return context.redirect('/');
    +		}
    +		// Auth token present, allow access
    +		return await next();
    +	}
    +
     	if (context.request.url.includes('/lorem')) {
     		context.locals.name = 'ipsum';
     	} else if (context.request.url.includes('/rewrite')) {
    
  • packages/astro/test/fixtures/middleware space/src/pages/admin.astro+6 0 added
    @@ -0,0 +1,6 @@
    +---
    +// Admin page - protected route
    +---
    +
    +<h1>Admin Panel</h1>
    +<p>Secret admin content</p>
    
  • packages/astro/test/middleware.test.js+50 0 modified
    @@ -94,6 +94,26 @@ describe('Middleware in DEV mode', () => {
     		assert.notEqual(headers.get('set-cookie'), null);
     	});
     
    +	describe('Path encoding in middleware', () => {
    +		it('should protect /admin route with auth check', async () => {
    +			const res = await fixture.fetch('/admin', { redirect: 'manual' });
    +			assert.equal(res.status, 302);
    +			assert.equal(res.headers.get('location'), '/');
    +		});
    +
    +		it('should NOT allow accessing /admin with url encoding', async () => {
    +			const res = await fixture.fetch('/%61dmin', { redirect: 'manual' });
    +			assert.equal(res.status, 302);
    +			assert.equal(res.headers.get('location'), '/');
    +		});
    +
    +		it('should NOT allow accessing /admin with fully encoded path', async () => {
    +			const res = await fixture.fetch('/%61%64%6d%69%6e', { redirect: 'manual' });
    +			assert.equal(res.status, 302);
    +			assert.equal(res.headers.get('location'), '/');
    +		});
    +	});
    +
     	describe('Integration hooks', () => {
     		it('Integration middleware marked as "pre" runs', async () => {
     			const res = await fixture.fetch('/integration-pre');
    @@ -335,6 +355,36 @@ describe('Middleware API in PROD mode, SSR', () => {
     		assert.equal(text.includes('<p>yes they can!</p>'), true);
     	});
     
    +	describe('Path encoding in middleware', () => {
    +		it('should allow accessing /admin with valid auth header', async () => {
    +			const request = new Request('http://example.com/admin', {
    +				headers: { Authorization: 'Bearer token123' },
    +			});
    +			const response = await app.render(request);
    +			assert.equal(response.status, 200);
    +			const html = await response.text();
    +			assert.equal(html.includes('Admin Panel'), true);
    +		});
    +
    +		it('should NOT allow accessing /admin without auth header', async () => {
    +			const request = new Request('http://example.com/admin');
    +			const response = await app.render(request);
    +			assert.equal(response.status, 302);
    +		});
    +
    +		it('should NOT allow accessing /admin with url encoding', async () => {
    +			const request = new Request('http://example.com/%61dmin');
    +			const response = await app.render(request);
    +			assert.equal(response.status, 302);
    +		});
    +
    +		it('should NOT allow accessing /admin with fully encoded path', async () => {
    +			const request = new Request('http://example.com/%61%64%6d%69%6e');
    +			const response = await app.render(request);
    +			assert.equal(response.status, 302);
    +		});
    +	});
    +
     	// keep this last
     	it('the integration should receive the path to the middleware', async () => {
     		fixture = await loadFixture({
    

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

6

News mentions

0

No linked articles in our index yet.