CVE-2025-1974 - Kubernetes/Ingress-NGINX O que você precisa saber
Overview
CVE-2025-1974 - O que você precisa saber
Olá! Para quem não sabe, eu era um dos mantenedores do Ingress NGINX até o fim do ano de 2024.
Eu percebi que ouve um barulho bem grande do recente CVE-2025-1974 e fiz uma live com o meu amigo Jeferson da Linuxtips para explicar um pouco melhor, da perspectiva de um mantenedor, o que aconteceu, o motivo de ter acontecido e o que está sendo feito.
Você pode encontrar a live aqui. Durante a live, eu usei um documento explicando alguns detalhes, bem como mostrei uma PoC de um webhook vulnerável, e a idéia é deixar aqui disponível para quem quiser entender melhor.
Recap de Ingress controllers e Ingress NGINX
Antes, vamos dar uma olhada no diagrama do Ingress NGINX:
Nesse diagrama, é importante mencionar:
- As setas em vermelho representam o tráfego de um usuário ou admin do Kubernetes, executando ações como “kubectl”, bem como tráfego entre o apiserver e o controller chamando os validation webhooks, bem como do controller para a API para ler os objetos de ingress, secrets e finalmente para configurar o arquivo nginx.conf do nosso proxy
- As setas em azul representam o tráfego do seu usuário, ou seja, quando você publica um Ingress, por onde seus usuários acessam o proxy e finalmente chegam nos Pods.
É importante mencionar que todas as vulnerabilidades são exploradas através do tráfego em vermelho. Ninguém chegando no seu Proxy/NGINX, nas portas 80/443 consegue explorar as vulnerabilidades a não ser que alguém tenha criado um Ingress malicioso.
Sobre permissões de acesso
O Ingress controller precisa de uma série de acessos para funcionar, tais como:
- Ingress
- Secret
- Configmap
- Node
- Namespaces
Por esse motivo, ao explorar uma vulnerabilidade no Ingress NGINX, veremos sempre que a principal preocupação é a extração de dados, uma vez que essas secrets contém certificados digitais, ou uma secret pode conter um token de acesso ao cluster.
Você encontra as permissões necessárias para o Ingress NGINX funcionar verificando o ClusterRole utilizado.
Por exemplo, digamos que alguém conseguiu explorar o seu Ingress NGINX e consegue executar comandos lá. Uma chamada poderia ser feita, com o Token da ServiceAccount do Ingress NGINX para listar todas as secrets do cluster:
1kubectl exec -it -n ingress-nginx deploy/ingress-nginx-controller -- /bin/sh
2... Aguarde o terminal abrir...
3curl -k -H \
4 "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
5 https://kubernetes.default/api/v1/namespaces/kube-system/secrets
Quando executamos esse comando, vemos que todas as secrets do namespace kube-system podem ser listadas com o token da service account Ingress NGINX
E onde está o problema?
Historicamente, o objeto Ingress no Kubernetes foi criado de forma muito simples. Isso fez com que a adição de novas funcionalidades fosse feita através de Annotations.
Por exemplo, um objeto simples de Ingress que implementa rewrite e catch all:
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: test-ingress
5 annotations:
6 nginx.ingress.kubernetes.io/use-regex: "true"
7spec:
8 ingressClassName: nginx
9 rules:
10 - host: test.com
11 http:
12 paths:
13 - path: /foo/.*
14 pathType: ImplementationSpecific
15 backend:
16 service:
17 name: test
18 port:
19 number: 80
O problema de se utilizar annotations, é que é muito difícil validar todas as entradas. Assim, passar strings com quebras de linha, aspas, e outras tentativas de modificar a configuração final do NGINX são possíveis. O Ingress NGINX possui alguns mecanismos de defesa para isso, como a Validação de annotations que utiliza regexes para saber se o que está sendo inserido realmente faz parte daquela funcionalidade, bem como é uma tentativa de bloquear eventuais tentativas de configurações maliciosas.
Mas nem sempre é possível… No Ingress NGINX, nós usamos um recurso da linguagem “Go” (que é a linguagem em que o Ingress Controller é desenvolvido), chamado de templates.
Basicamente, toda vez que alguém altera um Ingress, adicionando uma annotation, criando um novo serviço, todas as informações são lidas e renderizadas no arquivo nginx.conf. A falha em validar todas essas informações pode levar um usuário malicioso a escrever configurações arbitrárias no nginx.conf.
Olhando a lista de CVEs recentes, podemos ver que existe um padrão: Injeção de configuração
CVE | Descrição | Causa principal | Score |
---|---|---|---|
CVE-2021-25748 | Bypass de validação do campo ‘path’ | Falha na validação de input de annotation / injection de configuração | 6.5 |
CVE-2022-4886 | Bypass de validação do campo ‘path’ | Falha na validação de input de annotation / injection de configuração | 8.8 |
CVE-2023-5043 | Injeção de configurações via snippet annotation | Falha na validação de input de annotation / injection de configuração | 7.6 |
CVE-2024-7646 | Bypass do validador de annotations | Falha na validação de input de annotation / injection de configuração | 8.8 |
CVE-2025-24513 | Auth secret file path traversal vulnerability | Falha na validação de input do secret / injection de configuração | 4.8 |
CVE-2025-24514 | Configuration injection via unsanitized auth-url annotation | Falha na validação de input de annotation / injection de configuração | 8.8 |
CVE-2025-1097 | Configuration injection via unsanitized auth-tls-match-cn annotation | Falha na validação de input de annotation / injection de configuração | 8.8 |
CVE-2025-1098 | Configuration injection via unsanitized mirror annotations | Falha na validação de input de annotation / injection de configuração | 8.8 |
CVE-2025-1974 | ingress-nginx admission controller RCE escalation | Falha na validação + execução de código não validado durante o teste do admission webhook | 9.8 |
Mas por que score 9.8? É possível mesmo um usuário não autenticado explorar?
Sim e não. O que acontece é que a vulnerabilidade CVE-2025-1974 trata de uma falha no Webhook de validação.
Por exemplo, ao tentarmos criar um Ingress conforme abaixo, um erro irá acontecer pois trata-se de um ingress duplicado:
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: test-ingress1
5spec:
6 ingressClassName: nginx
7 rules:
8 - host: test.com
9 http:
10 paths:
11 - path: /foo/
12 pathType: Prefix
13 backend:
14 service:
15 name: test
16 port:
17 number: 80
18---
19apiVersion: networking.k8s.io/v1
20kind: Ingress
21metadata:
22 name: test-ingress2
23spec:
24 ingressClassName: nginx
25 rules:
26 - host: test.com
27 http:
28 paths:
29 - path: /foo/
30 pathType: Prefix
31 backend:
32 service:
33 name: test
34 port:
35 number: 80
e executar
1kubectl apply -f ingress-dup.yaml
2ingress.networking.k8s.io/test-ingress1 applied
3Error from server (BadRequest): error when creating "ingress-dup.yaml": admission webhook "validate.nginx.ingress.kubernetes.io" denied the request: host "test.com" and path "/foo/" is already defined in ingress default/test-ingress1
O webhook do Ingress NGINX é usado para, entre outras coisas:
- Validar se o
path
no spec é válido - Validar se não existe uma tentativa de criar um objeto de Ingress duplicado (mesmo host, mesmo path), em namespaces iguais ou diferentes
- Validar se a configuração solicitada não vai quebrar o NGINX, rodando um
nginx -t
na configuração renderizada
Esse último ítem é onde o problema é apresentado:
Um usuário com acesso ao endpoint do webhook de validação pode mandar uma request maliciosa para executar comandos no nginx
Então, primeiro ponto: você precisa ter acesso à API de Webhook, que não fica exposta de forma autenticada para fora do cluster ou na API do Kubernetes. Mas… se você tem acesso ao shell de um Pod rodando no Cluster, e não tem uma Network Policy que bloqueie seus Pods de acessar o endpoint do Webhook, as requisições podem ser feitas desse Pod.
E como isso acontece? Me de exemplos!
Vou demonstrar com um webhook de validação. Caso você tenha curiosidade em saber como criar um controller e webhooks, veja esse vídeo da V Körbes em uma live
Supondo que tenhamos o seguinte código vulnerável abaixo:
1func (v *YoloCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
2 yolo, ok := obj.(*vaicorinthiansv1.Yolo)
3 if !ok {
4 return nil, fmt.Errorf("expected a Yolo object but got %T", obj)
5 }
6 yololog.Info("Validation for Yolo upon creation", "name", yolo.GetName())
7
8 path := fmt.Sprintf("%s/%s", yolo.Namespace, yolo.Spec.Foo)
9 content, err := os.ReadFile(path)
10 if err != nil {
11 return nil, fmt.Errorf("erro ao ler o objeto yolo: %w", err)
12 }
13
14 if string(content) != "xpto" {
15 return nil, fmt.Errorf("o conteúdo do objeto yolo não é o esperado: %s", string(content))
16 }
17
18 return nil, nil
19}
Basicamente, esse código:
- Recebe a requisição de criação do objeto
yolo.vaicorinthians.rkatz.xyz
- Concatena o
namespace
e o campo.spec.foo
em uma string, que representa o caminho de um arquivo - Tenta ler o arquivo
- Caso a tentativa de ler o arquivo retorne um erro, retorna erro na validação
- Caso o conteúdo do arquivo não seja
xpto
, retorna um erro contendo o valor do arquivo - Caso contrário, autoriza a requisição
Ao criarmos o manifesto abaixo, receberemos um erro:
1kind: Yolo
2apiVersion: vaicorinthians.rkatz.xyz/v1
3metadata:
4 name: blo
5 namespace: default
6spec:
7 foo: bar
e executarmos:
1kubectl apply -f ~/tmp/ingresscve/yolo.yaml
2Error from server (Forbidden): error when creating "/Users/rkatz/tmp/ingresscve/yolo.yaml": admission webhook "vyolo-v1.kb.io" denied the request: erro ao ler o objeto yolo: open default/bar: no such file or directory
Opa, então quer dizer que eu posso tentar por exemplo um ataque de path transversal, usando o namespace?
Vamos tentar novamente, com o manifesto vulnerável abaixo:
1kind: Yolo
2apiVersion: vaicorinthians.rkatz.xyz/v1
3metadata:
4 name: blo
5 namespace: /var/run/secrets/kubernetes.io/serviceaccount/
6spec:
7 foo: token
Ao tentarmos com esse manifesto, um erro irá ocorrer. Isso porque o Kubernetes faz uma
validação inicial para verificar se o campo namespace
contém apenas caracteres válidos:
1kubectl apply -f ~/tmp/ingresscve/yolo-vuln.yaml
2error: error when retrieving current configuration of:
3Resource: "vaicorinthians.rkatz.xyz/v1, Resource=yolos", GroupVersionKind: "vaicorinthians.rkatz.xyz/v1, Kind=Yolo"
4Name: "blo", Namespace: "/var/run/secrets/kubernetes.io/serviceaccount/"
5from server for: "/Users/rkatz/tmp/ingresscve/yolo-vuln.yaml": invalid namespace "/var/run/secrets/kubernetes.io/serviceaccount/": [may not contain '/']
Mas existe um problema: o CVE menciona um acesso não autenticado ao webhook. Usando uma ferramenta chamada kube-review podemos criar uma solicitação de review, e encaminhar direto ao nosso webhook:
1kube-review create yolo-vuln.yaml > admission-yolo.json
Esse comando irá criar um payload JSON que pode ser enviado ao nosso webhook. Eu não irei colar o payload, mas você pode encontrar um similar no Github de um dos engenheiros da Wiz que reportou o CVE.
Logo, dentro de um Pod no meu cluster Kubernetes, caso eu execute o comando abaixo, eu estou enviando esse payload diretamente para o meu Webhook:
1curl -k -v https://webhooktest-webhook-service.webhooktest-system/validate-vaicorinthians-rkatz-xyz-v1-yolo -H "Content-Type: application/json" -d '@admission-yolo.json'
E o resultado? Eu acabei de conseguir o token de Service Account do meu controller, que pode ser usado para extrair informações:
1{"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1","response":{"allowed":false,"status":{"metadata":{},"message":"o conteúdo do objeto yolo não é o esperado: <<token-de-acesso>>","reason":"Forbidden","code":403}}}
Conclusões
A comunidade vem trabalhando para corrigir essas vulnerabilidades, bem como em um novo projeto chamado InGate cujo objetivo é reimplementar as funcionalidades do Ingress NGINX, porém usando o Gateway API e aplicando algumas das melhores práticas que não estavam disponíveis quando o Ingress NGINX foi criado, lá em 2014.
Porém esse não é um trabalho simples. A maioria dos mantenedores Open Source fazem isso em seu tempo livre, de forma voluntária. Por isso, seja legal com sua comunidade Open Source, e lembre-se de ter empatia, são seres humanos também :)
Adicionalmente, algumas medidas podem ser tomadas, como:
- Sempre, SEMPRE leia o Changelog dos componentes que você usa, podem existir Breaking Changes
- Evite ao máximo habilitar o uso de annotations
-snippet
no Ingress NGINX. Quando você habilita essas annotations, você basicamente está permitindo que seus usuários escrevam diretamente no arquivo nginx.conf sem qualquer validação - Algumas medidas defensivas como mecanismos de validação de annotations, ou um novo mecanismo de renderização de templates estão sendo criados. É importante verificar a documentação do Ingress NGINX para saber mais sobre as opções disponíveis
- Evite permitir aos seus usuários fazer configurações diretas no Kubernetes. Utilize-se ao invés disso de workflows, CI/CD, habilite a validação dos manifestos antes de permitir que o pipeline aplique eles
- Use mecanismos de
allow-list
para as annotations, de forma a permitir que seus usuários utilizem apenas funcionalidades que são realmente necessárias, e diminuindo a superfície de ataques - NetworkPolicies e outros recursos são seus amigos!
Referências e Acknowledges
Antes de mais nada, o grande acknowledge vai para a comunidade Ingress NGINX e Kubernetes. Lidar com esse CVE não foi fácil para eles, ainda mais com o ruído e repercursão que teve, e ainda assim eles trabalharam incessantemente por 3 meses para corrigir os problemas
Eu mencionei durante a live a existência de outros Ingress Controllers também, e eu tenho um carinho especial pelo Ingress HAProxy de meu amigo João Morais. Esse Ingress foi criado pelo João na época que trabalhavamos juntos no SERPRO para lidar com o volume de tráfego dos ambientes, e está lá até hoje atendendo suas requisições nos sistemas do Governo Brasileiro.
A Wiz foi extremamente responsável no reporte da vulnerabilidade e nos auxiliou em todas as etapas, eles escreveram um report muito bom que pode ser encontrado aqui