77 行
5.3 KiB
JSON
77 行
5.3 KiB
JSON
{
|
|
"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<img width=\"1920\" height=\"1025\" alt=\"image\" src=\"https://github.com/user-attachments/assets/7e0eeecd-607a-4c73-b12e-5977a30c9bc4\" />\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
|
|
}
|
|
}
|