You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
If the request path contains a percent-sequence that is not valid UTF-8 — e.g. %C0%AF (an overlong-UTF-8 encoding of /, extremely common in automated path-traversal / .env scanners) — decodeURI() throws URIError: URI malformed.
On-demand adapters call App.match() directly per request. For example @astrojs/cloudflare's server handler:
// @astrojs/cloudflare/dist/utils/handler.jsrouteData=app.match(request);// not wrapped — URIError escapes the worker `fetch`
Because the throw happens beforeapp.render(), it cannot be caught by user middleware (src/middleware.ts) either. The result is an uncaught exception → HTTP 500 (and, on Cloudflare, a logged worker exception) for any malformed request path, rather than a normal 404.
This is notable because Astro already handles this exact hazard gracefully everywhere else:
getPathnameFromRequest() in the same file wraps decodeURI in try/catch and falls back to the raw pathname:
So App.match() is the one decode path that still uses an unguarded decodeURI, bypassing both the try/catch pattern and validateAndDecodePathname.
What's the expected result?
A malformed/undecodable request path should resolve to a normal 404 (no matching route), consistent with the graceful fallback already used by getPathnameFromRequest() and normalizeUrl() — not an uncaught URIError/500.
What's the actual result?
decodeURI() throws URIError: URI malformed; the exception escapes the adapter's fetch handler. On @astrojs/cloudflare this surfaces as a worker exception and a 500. User middleware cannot intercept it because it is thrown before the render pipeline runs.
Link to Minimal Reproducible Example
Any deployment with an on-demand (prerender = false) route reproduces it. Against such a deployment:
curl -i 'https://<your-site>/..%C0%AF.env'# → 500 + "URIError: URI malformed" in adapter/worker logs
(Observed in production from automated .env scanners against an @astrojs/cloudflare Worker.)
Participation
I am willing to submit a pull request for this issue.
Suggested fix
Wrap the decodeURI calls in App.match() the same way getPathnameFromRequest() already does (or route them through validateAndDecodePathname with a graceful fallback), so a decode failure yields "no match" → 404 rather than an uncaught throw. Happy to open a PR mirroring the existing try/catch pattern.
Astro Info
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
App.match()(core/app/base.js) decodes the request pathname with a raw, unguardeddecodeURI()in three places:If the request path contains a percent-sequence that is not valid UTF-8 — e.g.
%C0%AF(an overlong-UTF-8 encoding of/, extremely common in automated path-traversal /.envscanners) —decodeURI()throwsURIError: URI malformed.On-demand adapters call
App.match()directly per request. For example@astrojs/cloudflare's server handler:Because the throw happens before
app.render(), it cannot be caught by user middleware (src/middleware.ts) either. The result is an uncaught exception → HTTP 500 (and, on Cloudflare, a logged workerexception) for any malformed request path, rather than a normal 404.This is notable because Astro already handles this exact hazard gracefully everywhere else:
getPathnameFromRequest()in the same file wrapsdecodeURIintry/catchand falls back to the raw pathname:normalizeUrl()(core/util/normalized-url.js) routes decoding throughvalidateAndDecodePathname()(added in v6.3.2, Reject double-encoded URL paths in pathname normalization #16556) and falls back without throwing on a plain decode failure.So
App.match()is the one decode path that still uses an unguardeddecodeURI, bypassing both thetry/catchpattern andvalidateAndDecodePathname.What's the expected result?
A malformed/undecodable request path should resolve to a normal 404 (no matching route), consistent with the graceful fallback already used by
getPathnameFromRequest()andnormalizeUrl()— not an uncaughtURIError/500.What's the actual result?
decodeURI()throwsURIError: URI malformed; the exception escapes the adapter'sfetchhandler. On@astrojs/cloudflarethis surfaces as a workerexceptionand a 500. User middleware cannot intercept it because it is thrown before the render pipeline runs.Link to Minimal Reproducible Example
Any deployment with an on-demand (
prerender = false) route reproduces it. Against such a deployment:(Observed in production from automated
.envscanners against an@astrojs/cloudflareWorker.)Participation
Suggested fix
Wrap the
decodeURIcalls inApp.match()the same waygetPathnameFromRequest()already does (or route them throughvalidateAndDecodePathnamewith a graceful fallback), so a decode failure yields "no match" → 404 rather than an uncaught throw. Happy to open a PR mirroring the existingtry/catchpattern.