{"id":"GHSA-m8p5-r7qv-cx4c","kind":"vulnerability","aliases":["GHSA-m8p5-r7qv-cx4c"],"related":[],"title":"Full-read SSRF in the default `/_image` endpoint via the same Host header","description":"### Root cause\r\n\r\n`astro/app/node` exports two request builders with opposite security postures:\r\n\r\n- `createRequest()` (line 123) validates the `Host` header against `security.allowedDomains` and\r\n  **falls back to `localhost`** when it does not match, with the comment\r\n  *\"Otherwise, fall back to 'localhost' to prevent SSRF attacks.\"*\r\n- `createRequestFromNodeRequest()` (line 51) takes `req.headers.host` **verbatim** as the URL\r\n  authority. No validation of any kind.\r\n\r\n`@astrojs/node` uses the unvalidated one on every request, in both serving modes:\r\n\r\n```\r\npackages/integrations/node/src/serve-app.ts:99    createRequestFromNodeRequest(req, {...})\r\npackages/integrations/node/src/serve-static.ts:73  createRequestFromNodeRequest(req, {...})\r\n```\r\n\r\nThe doc comment on `createRequestFromNodeRequest` justifies the omission:\r\n\r\n> Internal version of `createRequest` that skips X-Forwarded-* header validation.\r\n> Forwarded headers are resolved later inside `FetchState`, so doing it here too would\r\n> duplicate work on every request.\r\n\r\nThat is true for `X-Forwarded-*` but not for `Host`. `FetchState.#applyForwardedHeaders()`\r\nonly runs when `allowedDomains` is configured, and it only ever inspects\r\n`x-forwarded-proto` / `x-forwarded-host` / `x-forwarded-port`. It returns early when none\r\nare present, leaving `this.url` on the raw `Host`-derived origin. Nothing downstream\r\nre-validates `Host`.\r\n\r\nCommit `da8b5735` (\"Validate X-Forwarded-Host against allowedDomains in\r\ncreateRequestFromNodeRequest\") hardened the forwarded header in this exact function\r\nbut left `Host` untouched.\r\n\r\n### Why this contradicts the documentation\r\n\r\n`security.allowedDomains` is documented (`packages/astro/src/types/public/config.ts:700`) as:\r\n\r\n> This prevents host header injection attacks where malicious actors can manipulate the\r\n> `Astro.url` value by sending crafted `X-Forwarded-Host` headers.\r\n\r\n`Astro.url` is manipulable by the plain `Host` header, which `allowedDomains` never checks.\r\n\r\n### Proof\r\n\r\nConfig: default `@astrojs/node` standalone, `security.allowedDomains: [{ hostname: 'example.com', protocol: 'http' }]`.\r\n\r\nPage:\r\n```astro\r\n---\r\nconst origin = Astro.url.origin;\r\nconst absolute = new URL('/reset?token=abc', Astro.url).href;\r\n---\r\n<p>Astro.url.origin = {origin}</p>\r\n<p>absolute link    = {absolute}</p>\r\n```\r\n\r\n```\r\n$ curl -s http://127.0.0.1:4321/whoami\r\n<p>Astro.url.origin = http://127.0.0.1:4321</p>\r\n<p>absolute link    = http://127.0.0.1:4321/reset?token=abc</p>\r\n\r\n$ curl -s -H 'Host: attacker.example.net' http://127.0.0.1:4321/whoami\r\n<p>Astro.url.origin = http://attacker.example.net</p>\r\n<p>absolute link    = http://attacker.example.net/reset?token=abc</p>\r\n```\r\n\r\n### Impact\r\n\r\nEvery absolute URL an application builds from `Astro.url` (the documented way to do it)\r\npoints at an attacker-chosen host. Standard consequences: password-reset / email-confirmation\r\nlink poisoning, poisoned canonical and `og:` tags, and cache poisoning where a shared cache\r\nkeys on path but not `Host`. The failure is silent — `allowedDomains` gives operators the\r\nimpression the value is validated.","summary":"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.","severity":"medium","cvss":null,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"@astrojs/node","fixed":["11.1.7"],"affected":"<= 11.1.6","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["astro","caching","cwe-918","node","package","password-reset","self-hosted"],"not_affected_if":["not-using-node-adapter"],"how_to_check":"- Run `npm ls @astrojs/node` and confirm the installed version is `11.1.6` or lower.\n- Search your pages and endpoints for uses of `Astro.url` that build absolute links, canonical tags, or reset and confirmation URLs.\n- Check whether a shared cache or CDN keys on path but not on `Host`.","fix":"- `@astrojs/node` `11.1.7`","status":"open","published_at":"2026-10-12T18:20:10+00:00","first_seen_at":"2026-11-09T22:53:17+00:00","triaged_at":"2026-11-09T22:59:08+00:00","updated_at":"2026-11-09T22:59:08+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:59:08Z","model":"claude-haiku-5-5","output":{"fix":"- `@astrojs/node` `11.1.7`","reach":"internet","title":"Astro Node adapter trusts Host header, letting attackers poison app URLs","impact":"medium","summary":"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.","audience":"developers","category":"package","nicknames":[],"who_cares":["astro","self-hosted","node","password-reset","caching"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"Attacker-chosen hosts appear in emailed links and cached pages served to users","analyst_note":"","how_to_check":"- Run `npm ls @astrojs/node` and confirm the installed version is `11.1.6` or lower.\n- Search your pages and endpoints for uses of `Astro.url` that build absolute links, canonical tags, or reset and confirmation URLs.\n- Check whether a shared cache or CDN keys on path but not on `Host`.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["not-using-node-adapter"],"who_is_affected":"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.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"no","severity_if_unknown":"medium"},"cost_usd":0.001323,"input_tokens":5743,"output_tokens":1497,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-918':29B '/_image':8A '/reset':346C '/reset?token=abc':382C '123':101C '127.0.0.1':358C,361C,365C,374C '4321':362C '4321/reset':366C '4321/whoami':359C,375C '51':132C '700':291C '73':162C '99':158C 'abc':348C,368C 'absolut':67B,343C,353C,355C,363C,378C,385C 'actor':301C 'adapt':40B 'alloweddomain':218C,267C,324C,437C 'applic':388C 'applyforwardedhead':214C 'astro':26B,338C 'astro.url':64B,305C,315C,349C,391C 'astro.url.origin':341C,351C,360C,376C 'astro/app/node':90C 'astrojs/node':25A,39B,145C,330C 'attack':56B,76B,129C,298C,402C 'attacker-chosen':75B,401C 'attacker.example.net':373C,377C,381C 'attacker.example.net/reset?token=abc':380C 'author':139C 'back':110C,123C 'build':41B,389C 'builder':94C 'built':69B 'cach':27B,86B,421C,426C 'canon':416C 'caus':89C 'check':51B,326C 'chosen':77B,403C 'comment':120C,167C 'commit':259C 'config':328C 'configur':220C 'confirm':412C 'consequ':406C 'const':339C,342C 'contradict':284C 'craft':60B,309C 'createrequest':99C,176C 'createrequestfromnoderequest':130C,159C,163C,169C,269C 'curl':356C,369C 'cwe':28B 'cx4c':19A,24A 'da8b5735':260C 'default':7A,329C 'deriv':251C 'doc':166C 'document':286C,289C,393C 'downstream':254C 'duplic':197C 'earli':240C 'email':411C 'email-confirm':410C 'enabl':79B 'endpoint':9A 'ever':224C 'everi':66B,151C,200C,384C 'exact':276C 'example.com':334C 'export':91C 'failur':434C 'fall':109C,122C 'fetchstat':190C,213C 'forward':181C,184C,208C,228C,232C,236C,264C,272C,312C 'full':2A 'full-read':1A 'function':277C 'ghsa':16A,21A 'ghsa-m8p5-r7qv-cx4c':15A,20A 'give':438C 'h':371C 'harden':270C 'header':14A,49B,62B,105C,182C,185C,273C,296C,314C,322C 'host':13A,37B,48B,61B,78B,104C,212C,233C,250C,258C,265C,280C,295C,313C,321C,372C,404C,432C 'hostnam':333C 'href':350C 'http':336C 'impact':383C 'impress':441C 'inject':297C 'insid':189C 'inspect':225C 'intern':173C 'justifi':170C 'key':427C 'kind':144C 'later':188C 'leav':245C 'left':279C 'line':100C,131C 'link':68B,83B,354C,364C,379C,413C 'localhost':112C,125C 'm8p5':17A,22A 'malici':300C 'manipul':303C,317C 'match':117C 'mode':156C 'never':325C 'new':344C 'node':30B 'none':242C 'noth':253C 'og':418C 'omiss':172C 'one':149C 'oper':439C 'opposit':96C 'origin':252C,340C,352C 'otherwis':121C 'packag':31B 'packages/astro/src/types/public/config.ts':290C 'packages/integrations/node/src/serve-app.ts':157C 'packages/integrations/node/src/serve-static.ts':161C 'page':337C 'password':33B,81B,408C 'password-reset':32B,80B,407C 'path':429C 'plain':320C 'point':72B,398C 'poison':84B,87B,414C,415C,422C 'port':237C 'postur':98C 'present':244C 'prevent':127C,294C 'proof':327C 'proto':229C 'protocol':335C 'r7qv':18A,23A 'raw':249C 're':256C 're-valid':255C 'read':3A 'req':160C,164C 'req.headers.host':134C 'request':43B,93C,152C,201C 'reset':34B,82B,409C 'resolv':187C 'return':239C 'root':88C 'run':216C 'secur':97C 'security.alloweddomains':54B,107C,287C,332C 'self':36B 'self-host':35B 'send':58B,308C 'serv':155C 'share':425C 'silent':436C 'skip':178C 'ssrf':4A,128C 'standalon':331C 'standard':405C 'straight':45B 'tag':419C 'take':133C 'this.url':246C 'token':347C,367C 'true':204C 'two':92C 'untouch':281C 'unvalid':148C 'url':44B,138C,345C,386C 'use':146C 'valid':102C,141C,183C,257C,261C,445C 'valu':306C,443C 'verbatim':135C 'version':174C 'via':10A 'way':394C 'without':50B 'work':198C 'would':196C 'x':180C,207C,227C,231C,235C,263C,311C 'x-forward':179C,206C 'x-forwarded-host':230C,262C,310C 'x-forwarded-port':234C 'x-forwarded-proto':226C","nicknames":[],"mitigations":[],"who_affected":"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.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["node"],"ptokens":["astrojs","node"],"labels":["astro","caching","cwe918","medium","node","npm","package","passwordreset","selfhosted"],"campaign_type":null,"search_a":"'/_image':8A 'astrojs/node':25A 'cx4c':19A,24A 'default':7A 'endpoint':9A 'full':2A 'full-read':1A 'ghsa':16A,21A 'ghsa-m8p5-r7qv-cx4c':15A,20A 'header':14A 'host':13A 'm8p5':17A,22A 'r7qv':18A,23A 'read':3A 'ssrf':4A 'via':10A","search_b":"'-918':29B 'absolut':67B 'adapt':40B 'astro':26B 'astro.url':64B 'astrojs/node':39B 'attack':56B,76B 'attacker-chosen':75B 'build':41B 'built':69B 'cach':27B,86B 'check':51B 'chosen':77B 'craft':60B 'cwe':28B 'enabl':79B 'everi':66B 'header':49B,62B 'host':37B,48B,61B,78B 'link':68B,83B 'node':30B 'packag':31B 'password':33B,81B 'password-reset':32B,80B 'point':72B 'poison':84B,87B 'request':43B 'reset':34B,82B 'security.alloweddomains':54B 'self':36B 'self-host':35B 'send':58B 'straight':45B 'url':44B 'without':50B","search_c":"'/reset':346C '/reset?token=abc':382C '123':101C '127.0.0.1':358C,361C,365C,374C '4321':362C '4321/reset':366C '4321/whoami':359C,375C '51':132C '700':291C '73':162C '99':158C 'abc':348C,368C 'absolut':343C,353C,355C,363C,378C,385C 'actor':301C 'alloweddomain':218C,267C,324C,437C 'applic':388C 'applyforwardedhead':214C 'astro':338C 'astro.url':305C,315C,349C,391C 'astro.url.origin':341C,351C,360C,376C 'astro/app/node':90C 'astrojs/node':145C,330C 'attack':129C,298C,402C 'attacker-chosen':401C 'attacker.example.net':373C,377C,381C 'attacker.example.net/reset?token=abc':380C 'author':139C 'back':110C,123C 'build':389C 'builder':94C 'cach':421C,426C 'canon':416C 'caus':89C 'check':326C 'chosen':403C 'comment':120C,167C 'commit':259C 'config':328C 'configur':220C 'confirm':412C 'consequ':406C 'const':339C,342C 'contradict':284C 'craft':309C 'createrequest':99C,176C 'createrequestfromnoderequest':130C,159C,163C,169C,269C 'curl':356C,369C 'da8b5735':260C 'default':329C 'deriv':251C 'doc':166C 'document':286C,289C,393C 'downstream':254C 'duplic':197C 'earli':240C 'email':411C 'email-confirm':410C 'ever':224C 'everi':151C,200C,384C 'exact':276C 'example.com':334C 'export':91C 'failur':434C 'fall':109C,122C 'fetchstat':190C,213C 'forward':181C,184C,208C,228C,232C,236C,264C,272C,312C 'function':277C 'give':438C 'h':371C 'harden':270C 'header':105C,182C,185C,273C,296C,314C,322C 'host':104C,212C,233C,250C,258C,265C,280C,295C,313C,321C,372C,404C,432C 'hostnam':333C 'href':350C 'http':336C 'impact':383C 'impress':441C 'inject':297C 'insid':189C 'inspect':225C 'intern':173C 'justifi':170C 'key':427C 'kind':144C 'later':188C 'leav':245C 'left':279C 'line':100C,131C 'link':354C,364C,379C,413C 'localhost':112C,125C 'malici':300C 'manipul':303C,317C 'match':117C 'mode':156C 'never':325C 'new':344C 'none':242C 'noth':253C 'og':418C 'omiss':172C 'one':149C 'oper':439C 'opposit':96C 'origin':252C,340C,352C 'otherwis':121C 'packages/astro/src/types/public/config.ts':290C 'packages/integrations/node/src/serve-app.ts':157C 'packages/integrations/node/src/serve-static.ts':161C 'page':337C 'password':408C 'password-reset':407C 'path':429C 'plain':320C 'point':398C 'poison':414C,415C,422C 'port':237C 'postur':98C 'present':244C 'prevent':127C,294C 'proof':327C 'proto':229C 'protocol':335C 'raw':249C 're':256C 're-valid':255C 'req':160C,164C 'req.headers.host':134C 'request':93C,152C,201C 'reset':409C 'resolv':187C 'return':239C 'root':88C 'run':216C 'secur':97C 'security.alloweddomains':107C,287C,332C 'send':308C 'serv':155C 'share':425C 'silent':436C 'skip':178C 'ssrf':128C 'standalon':331C 'standard':405C 'tag':419C 'take':133C 'this.url':246C 'token':347C,367C 'true':204C 'two':92C 'untouch':281C 'unvalid':148C 'url':138C,345C,386C 'use':146C 'valid':102C,141C,183C,257C,261C,445C 'valu':306C,443C 'verbatim':135C 'version':174C 'way':394C 'work':198C 'would':196C 'x':180C,207C,227C,231C,235C,263C,311C 'x-forward':179C,206C 'x-forwarded-host':230C,262C,310C 'x-forwarded-port':234C 'x-forwarded-proto':226C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-12T18:20:10+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-m8p5-r7qv-cx4c","url":"https://github.com/withastro/astro/security/advisories/GHSA-m8p5-r7qv-cx4c","severity":"medium","published_at":"2026-10-12T18:20:10+00:00","modified_at":"2026-10-12T18:20:10+00:00","first_seen_at":"2026-11-09T22:53:17+00:00","fetched_at":"2026-11-09T22:53:17+00:00","note":null}],"documents":[{"id":110452,"url":"https://github.com/withastro/astro/security/advisories/GHSA-m8p5-r7qv-cx4c","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}