Skip to content

Commit 02ee246

Browse files
1 parent ab876a4 commit 02ee246

File tree

3 files changed

+13
-7
lines changed

3 files changed

+13
-7
lines changed

advisories/github-reviewed/2025/11/GHSA-fvmw-cj7j-j39q/GHSA-fvmw-cj7j-j39q.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-fvmw-cj7j-j39q",
4-
"modified": "2025-11-21T15:31:27Z",
4+
"modified": "2025-11-27T08:15:37Z",
55
"published": "2025-11-19T20:09:12Z",
66
"aliases": [
77
"CVE-2025-65019"
88
],
9-
"summary": "Astro Cloudflare adapter has Stored Cross Site Scripting vulnerability in /_image endpoint",
9+
"summary": "Astro Cloudflare adapter has Stored Cross-site Scripting vulnerability in /_image endpoint",
1010
"details": "**Summary** \nA Cross-Site Scripting (XSS) vulnerability exists in Astro when using the **@astrojs/cloudflare** adapter with `output: 'server'`. The built-in image optimization endpoint (`/_image`) uses `isRemoteAllowed()` from Astro’s internal helpers, which **unconditionally allows `data:` URLs**. When the endpoint receives a valid `data:` URL pointing to a malicious SVG containing JavaScript, and the Cloudflare-specific implementation performs a **302 redirect back to the original `data:` URL**, the browser directly executes the embedded JavaScript. This completely bypasses any domain allow-listing (`image.domains` / `image.remotePatterns`) and typical Content Security Policy mitigations.\n\n**Affected Versions** \n- `@astrojs/cloudflare` ≤ 12.6.10 (and likely all previous versions) \n- Astro ≥ 4.x when used with `output: 'server'` and the Cloudflare adapter\n\n**Root Cause – Vulnerable Code** \nFile: `node_modules/@astrojs/internal-helpers/src/remote.ts`\n\n```ts\nexport function isRemoteAllowed(src: string, ...): boolean {\n if (!URL.canParse(src)) {\n return false;\n }\n const url = new URL(src);\n\n // Data URLs are always allowed \n if (url.protocol === 'data:') {\n return true;\n }\n\n // Non-http(s) protocols are never allowed\n if (!['http:', 'https:'].includes(url.protocol)) {\n return false;\n }\n // ... further http/https allow-list checks\n}\n```\n\nIn the **Cloudflare adapter**, the `/_image` endpoint contains logic similar to:\n\n```ts\n\tconst href = ctx.url.searchParams.get('href');\n\tif (!href) {\n\t\t// return error \n\t}\n\n\tif (isRemotePath(href)) {\n\t\tif (isRemoteAllowed(href, imageConfig) === false) {\n\t\t\t// return error\n\t\t} else {\n //redirect to return the image \n\t\t\treturn Response.redirect(href, 302);\n\t\t}\n\t}\n```\n\nBecause `data:` URLs are considered “allowed”, a request such as: \n`https://example.com/_image?href=data:image/svg+xml;base64,PHN2Zy... (base64-encoded malicious SVG)` \n\ntriggers a **302 redirect directly to the `data:` URL**, causing the browser to render and execute the malicious JavaScript inside the SVG.\n\n**Proof of Concept (PoC)** \n\n1. Create a minimal Astro project with Cloudflare adapter (`output: 'server'`).\n2. Deploy to Cloudflare Pages or Workers.\n3. Request the image endpoint with the following payload:\n\n```\nhttps://yoursite.com/_image?href=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxzY3JpcHQ+YWxlcnQoJ3pvbWFzZWMnKTwvc2NyaXB0Pjwvc3ZnPg==\n```\n\n (Base64 decodes to: `<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert('zomasec')</script></svg>`)\n\n4. The endpoint returns a **302 redirect** to the `data:` URL → browser executes the `<script>` → `alert()` fires.\n\n**Impact** \n- Reflected/Strored XSS (depending on application usage) \n- Session hijacking (access to cookies, localStorage, etc.) \n- Account takeover when combined with CSRF \n- Data exfiltration to attacker-controlled servers \n- Bypasses `image.domains` / `image.remotePatterns` configuration entirely \n\n**Safe vs Vulnerable Behavior** \nOther Astro adapters (Node, Vercel, etc.) typically **proxy and rasterize** SVGs, stripping JavaScript. The **Cloudflare adapter** currently **redirects** to remote resources (including `data:` URLs), making it uniquely vulnerable.\n\n**References** \n- Vulnerable function: https://github.com/withastro/astro/blob/main/packages/internal-helpers/src/remote.ts \n- Similar `data:` URL bypass in WordPress: [CVE-2025-2575 ](https://feedly.com/cve/CVE-2025-2575)",
1111
"severity": [
1212
{

advisories/github-reviewed/2025/11/GHSA-ggxq-hp9w-j794/GHSA-ggxq-hp9w-j794.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-ggxq-hp9w-j794",
4-
"modified": "2025-11-19T20:03:21Z",
4+
"modified": "2025-11-27T08:14:43Z",
55
"published": "2025-11-19T20:03:21Z",
66
"aliases": [
77
"CVE-2025-64765"
88
],
99
"summary": "Astro's middleware authentication checks based on url.pathname can be bypassed via url encoded values",
10-
"details": "A mismatch exists between how Astro normalizes request paths for routing/rendering and how the application’s 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\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\nThankyou, let me know if any more info is needed happy to help :)",
10+
"details": "A mismatch exists between how Astro normalizes request paths for routing/rendering and how the application’s 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 :)",
1111
"severity": [
1212
{
1313
"type": "CVSS_V4",

advisories/github-reviewed/2025/11/GHSA-m449-cwjh-6pw7/GHSA-m449-cwjh-6pw7.json

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"schema_version": "1.4.0",
33
"id": "GHSA-m449-cwjh-6pw7",
4-
"modified": "2025-11-24T22:42:07Z",
4+
"modified": "2025-11-27T08:15:59Z",
55
"published": "2025-11-24T22:42:07Z",
6-
"aliases": [],
6+
"aliases": [
7+
"CVE-2025-66019"
8+
],
79
"summary": "pypdf's LZWDecode streams be manipulated to exhaust RAM",
810
"details": "### Impact\n\nAn attacker who uses this vulnerability can craft a PDF which leads to a memory usage of up to 1 GB per stream. This requires parsing the content stream of a page using the LZWDecode filter.\n\nThis is a follow up to [GHSA-jfx9-29x2-rv3j](https://github.com/py-pdf/pypdf/security/advisories/GHSA-jfx9-29x2-rv3j) to align the default limit with the one for *zlib*.\n\n### Patches\nThis has been fixed in [pypdf==6.4.0](https://github.com/py-pdf/pypdf/releases/tag/6.4.0).\n\n### Workarounds\nIf users cannot upgrade yet, use the line below to overwrite the default in their code:\n\n```python\npypdf.filters.LZW_MAX_OUTPUT_LENGTH = 75_000_000\n```",
911
"severity": [
@@ -42,6 +44,10 @@
4244
"type": "WEB",
4345
"url": "https://github.com/py-pdf/pypdf/security/advisories/GHSA-m449-cwjh-6pw7"
4446
},
47+
{
48+
"type": "ADVISORY",
49+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66019"
50+
},
4551
{
4652
"type": "WEB",
4753
"url": "https://github.com/py-pdf/pypdf/commit/96186725e5e6f237129a58a97cd19204a9ce40b2"
@@ -63,6 +69,6 @@
6369
"severity": "MODERATE",
6470
"github_reviewed": true,
6571
"github_reviewed_at": "2025-11-24T22:42:07Z",
66-
"nvd_published_at": null
72+
"nvd_published_at": "2025-11-26T00:15:51Z"
6773
}
6874
}

0 commit comments

Comments
 (0)