Fix server fetches a URL the caller controls in Astro

An outbound HTTP request is made to a URL that came from the caller. The server can reach hosts the caller cannot - cloud metadata endpoints, internal admin services, databases bound to localhost - so this turns the server into a proxy into its own network. Validate the destination against an allowlist before fetching it.

high likely Astro CWE-918 / OWASP A10:2021

The vulnerable pattern in Astro

HIGH likely Server fetches a URL the caller controls A10:2021 src/pages/api/proxy.ts:14:28 12 │ // ssrf: the server fetches whatever host the caller names. 13 │ if (target) { 14 │ const upstream = await fetch(target) │ ~~~~~~~~~~~~~ destination chosen by the caller 15 │ return new Response(JSON.stringify(await upstream.json()), { 16 │ headers: { 'Content-Type': 'application/json' },

This finding comes from the Astro fixture in the owlwarden test suite, in /api/proxy. The destination of this request comes from the caller, so they choose which host the server connects to. That includes hosts they cannot reach themselves: the cloud metadata endpoint that hands out IAM credentials, internal services that skip authentication because they are 'not exposed', and anything bound to localhost.

The corrected handler

Validate before fetching in the API route, and refuse redirects.

const { url } = await request.json()
const target = assertAllowedUrl(url)
const upstream = await fetch(target, { redirect: 'error' })

On a different runtime

The fix above is written for the runtime Astro usually runs on. These are the runtimes where it would not run at all - an import that does not exist, or an API the host does not have - and what to write instead.

Workers and other fetch-API runtimes

redirect: 'error' is not honoured on this runtime. Use redirect: 'manual' and refuse the response yourself, or an allowlisted host can redirect you to one that is not.

const url = assertAllowedUrl(input)
const upstream = await fetch(url, { redirect: 'manual' })
if (upstream.status >= 300 && upstream.status < 400) {
  throw new Error('refusing to follow a redirect from an allowlisted host')
}

If you are not using Astro

Check the destination against an allowlist of hosts before fetching it. Blocklists do not work here: DNS rebinding, redirects, and IPv6-mapped addresses all defeat them.

Check your own repository

npx owlwarden scan
npx owlwarden explain ssrf

Runs on your machine. No account, no telemetry, no network unless you ask. In CI, SARIF uploads to code scanning and the exit code is the gate.

Other Astro checks

Rules with a tested Astro example.

ssrf for every framework / All rules / owlwarden