@ASTROJS/NODE · GHSA-m8p5-r7qv-cx4c · Package
FIX → 11.1.7
The @astrojs/node adapter builds the request URL straight from the Host header without checking it against security.allowedDomains. An attacker can send a crafted Host header so Astro.url and every absolute link built from it point to an attacker-chosen host, enabling password-reset link poisoning and cache poisoning.
first public Oct 12, 2026 · 1 source
Could this affect your app?
Free first check from your website. Connect your repo for the full answer.
Sites built with Astro that run on the default @astrojs/node adapter in standalone or static-serving mode are exposed. The risk applies when the app builds absolute URLs from Astro.url, such as reset links, email confirmations, canonical or og: tags. Sites that do not use the Node adapter or build no absolute URLs from Astro.url can skip it.
Not affected if not-using-node-adapter
Who cares astro, caching, node, password-reset, self-hosted
npm ls @astrojs/node and confirm the installed version is 11.1.6 or lower.Astro.url that build absolute links, canonical tags, or reset and confirmation URLs.Host.Upgrade to 11.1.7
astro/app/node exports two request builders with opposite security postures:
createRequest() (line 123) validates the Host header against security.allowedDomains and
falls back to localhost when it does not match, with the comment
"Otherwise, fall back to 'localhost' to prevent SSRF attacks."createRequestFromNodeRequest() (line 51) takes req.headers.host verbatim as the URL
authority. No validation of any kind.@astrojs/node uses the unvalidated one on every request, in both serving modes:
packages/integrations/node/src/serve-app.ts:99 createRequestFromNodeRequest(req, {...})
packages/integrations/node/src/serve-static: (req, {...})
Check it with your coding agent
It reads this page and tells you if your code is affected.
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
@astrojs/node 11.1.7The doc comment on createRequestFromNodeRequest justifies the omission:
Internal version of
createRequestthat skips X-Forwarded-* header validation. Forwarded headers are resolved later insideFetchState, so doing it here too would duplicate work on every request.
That is true for X-Forwarded-* but not for Host. FetchState.#applyForwardedHeaders()
only runs when allowedDomains is configured, and it only ever inspects
x-forwarded-proto / x-forwarded-host / x-forwarded-port. It returns early when none
are present, leaving this.url on the raw Host-derived origin. Nothing downstream
re-validates Host.
Commit da8b5735 ("Validate X-Forwarded-Host against allowedDomains in
createRequestFromNodeRequest") hardened the forwarded header in this exact function
but left Host untouched.
security.allowedDomains is documented (packages/astro/src/types/public/config.ts:700) as:
This prevents host header injection attacks where malicious actors can manipulate the
Astro.urlvalue by sending craftedX-Forwarded-Hostheaders.
Astro.url is manipulable by the plain Host header, which allowedDomains never checks.
Config: default @astrojs/node standalone, security.allowedDomains: [{ hostname: 'example.com', protocol: 'http' }].
Page:
---
const origin = Astro.url.origin;
const absolute = new URL('/reset?token=abc', Astro.url).href;
---
<p>Astro.url.origin = {origin}</p>
<p>absolute link = {absolute}</p>
$ curl -s http://127.0.0.1:4321/whoami
<p>Astro.url.origin = http://127.0.0.1:4321</p>
<p>absolute link = http://127.0.0.1:4321/reset?token=abc</p>
$ curl -s -H 'Host: attacker.example.net' http://127.0.0.1:4321/whoami
<p>Astro.url.origin = http://attacker.example.net</p>
<p>absolute link = http://attacker.example.net/reset?token=abc</p>
Every absolute URL an application builds from Astro.url (the documented way to do it)
points at an attacker-chosen host. Standard consequences: password-reset / email-confirmation
link poisoning, poisoned canonical and og: tags, and cache poisoning where a shared cache
keys on path but not Host. The failure is silent — allowedDomains gives operators the
impression the value is validated.