{ "canonical_id": "astro--CVE-2025-64765", "system_id": "astro", "display_name": "Astro", "category": "frameworks", "advisory_mode": "core", "title": "Astro's middleware authentication checks based on url.pathname can be bypassed via url encoded values", "summary": "A mismatch exists between how Astro normalizes request paths for routing/rendering and how the application\u2019s middleware reads the path for validation checks. Astro internally applies `decodeURI()` to determine which route to render, while the middleware uses `context.url.pathname` without applying the same normalization (decodeURI).\n\nThis discrepancy may allow attackers to reach protected routes (e.g., /admin) using encoded path variants that pass routing but bypass validation checks.\n\nhttps://github.com/withastro/astro/blob/ebc4b1cde82c76076d5d673b5b70f94be2c066f3/packages/astro/src/vite-plugin-astro-server/request.ts#L40-L44\n\n```js\n/** The main logic to route dev server requests to pages in Astro. */\nexport async function handleRequest({\n pipeline,\n routesList,\n controller,\n incomingRequest,\n incomingResponse,\n}: HandleRequest) {\n const { config, loader } = pipeline;\n const origin = `${loader.isHttps() ? 'https' : 'http'}://${\n incomingRequest.headers[':authority'] ?? incomingRequest.headers.host\n }`;\n\n const url = new URL(origin + incomingRequest.url);\n let pathname: string;\n if (config.trailingSlash === 'never' && !incomingRequest.url) {\n pathname = '';\n } else {\n // We already have a middleware that checks if there's an incoming URL that has invalid URI, so it's safe\n // to not handle the error: packages/astro/src/vite-plugin-astro-server/base.ts\n pathname = decodeURI(url.pathname); // here this url is for routing/rendering\n }\n\n // Add config.base back to url before passing it to SSR\n url.pathname = removeTrailingForwardSlash(config.base) + url.pathname; // this is used for middleware context\n```\n\nConsider an application having the following middleware code:\n\n```js\nimport { defineMiddleware } from \"astro/middleware\";\n\nexport const onRequest = defineMiddleware(async (context, next) => {\n const isAuthed = false; // simulate no auth\n if (context.url.pathname === \"/admin\" && !isAuthed) {\n return context.redirect(\"/\");\n }\n return next();\n});\n```\n\n`context.url.pathname` is validated , if it's equal to `/admin` the `isAuthed` property must be true for the next() method to be called. The same example can be found in the official docs https://docs.astro.build/en/guides/authentication/\n\n\n`context.url.pathname` returns the raw version which is `/%61admin` while pathname which is used for routing/rendering `/admin`, this creates a path normalization mismatch.\n\nBy sending the following request, it's possible to bypass the middleware check\n\n```\nGET /%61dmin HTTP/1.1\nHost: localhost:3000\n```\n\n\"image\"\n\n\n**Remediation**\n\nEnsure middleware context has the same normalized pathname value that Astro uses internally, because any difference could allow it to bypass such checks. In short maybe something like this\n\n```diff\n pathname = decodeURI(url.pathname);\n }\n\n // Add config.base back to url before passing it to SSR\n- url.pathname = removeTrailingForwardSlash(config.base) + url.pathname;\n+ url.pathname = removeTrailingForwardSlash(config.base) + decodeURI(url.pathname);\n```\n\nThank you, let @Sudistark know if any more info is needed. Happy to help :)", "published_at": "2025-11-19T20:03:21Z", "updated_at": "2026-02-04T03:01:27.986221Z", "severity": "medium", "cvss_score": 4.0, "exploit_status": "unknown", "source_confidence": "official", "official_source_url": "https://github.com/withastro/astro/security/advisories/GHSA-ggxq-hp9w-j794", "secondary_source_urls": [ "https://nvd.nist.gov/vuln/detail/CVE-2025-64765", "https://github.com/withastro/astro/commit/6f800813516b07bbe12c666a92937525fddb58ce", "https://github.com/withastro/astro" ], "aliases": [ "CVE-2025-64765", "GHSA-ggxq-hp9w-j794" ], "cve_ids": [ "CVE-2025-64765" ], "ghsa_ids": [ "GHSA-ggxq-hp9w-j794" ], "osv_ids": [ "GHSA-ggxq-hp9w-j794" ], "affected_versions": [ "introduced=0, fixed<5.15.8" ], "fixed_versions": [ "5.15.8" ], "package_name": "astro", "render_markdown": true, "case_path": "07-framework-security/frameworks/astro/cases/astro-cve-2025-64765.md", "secure_code_topics": [ "authz-server-side-recheck", "csp-trusted-types", "plugin-extension-trust-policy", "dependency-upgrade-policy", "proxy-trust-boundary" ], "status": "generated", "triage_reasons": [], "verification_status": "triage-manual", "verification_mode": "synthetic", "last_verified_at": null, "last_run_id": null, "evidence_bundle": null, "historical_status": null, "latest_status": null, "browser_evidence": { "required": false, "present": false, "refs": [] }, "repro_profile_id": "file-upload-generic", "artifact_mode": "synthetic", "blocked_reason": null, "metadata": { "source_names": [ "OSV Astro" ], "source_kinds": [ "osv-batch" ], "candidate_count": 1 } }