
Prova de conceito para CVE-2025-23211; reproduz a vulnerabilidade relacionada à privacidade de dados para apoiar a validação de segurança autorizada, testes de patch e verificação de mitigação.
Um ambiente autocontido e reproduzível que demonstra o CVE-2025-23211, uma vulnerabilidade de injeção de template do lado do servidor (SSTI) no Tandoor Recipes que leva à execução remota de código. O repositório sobe a versão vulnerável, mostra o exploit, depois sobe a versão corrigida e mostra a mesma entrada sendo rejeitada.
| CVE | CVE-2025-23211 |
| Advisory | GHSA-r6rj-h75w-vj8v |
| Class | Injeção de Template do Lado do Servidor (Jinja2), CWE-94 / CWE-1336 |
| CVSS 3.1 | 9.9 (Crítico) - AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
| Afetado | Tandoor Recipes <= 1.5.23 |
| Corrigido em | 1.5.24 |
Tandoor Recipes é uma aplicação de código aberto, auto-hospedada, para planejamento de refeições e gerenciamento de receitas. O backend é Django (Python) servido pelo Gunicorn, o frontend é uma aplicação de página única em Vue.js, e os dados são armazenados no PostgreSQL.
O Tandoor permite que os usuários escrevam expressões de template Jinja2 dentro do texto de instruções de uma receita, um recurso destinado a tarefas como escalonamento de quantidades de ingredientes. Essas instruções são renderizadas no servidor.
O texto das instruções é processado por render_instructions() em
cookbook/helper/template_helper.py. Nas versões afetadas, o texto é passado para o Template() padrão do Jinja2 e renderizado sem sandbox:
template = Template(instructions)
instructions = template.render(ingredients=ingredients, scale=scale)
Como o ambiente Jinja2 padrão expõe o grafo de objetos do Python, um usuário autenticado que pode editar uma receita pode injetar uma expressão de template que percorre de uma tupla vazia até subprocess.Popen e executa um comando arbitrário no servidor. O contêiner padrão é executado como root, então o comando é executado como root.
As instruções passam por uma etapa de Markdown antes que o Jinja2 as renderize, e essa etapa remove underscores brutos. O exploit, portanto, codifica os underscores em hexadecimal (\x5f\x5f) e usa o filtro attr() em vez da notação de ponto. Veja payloads.txt.
compose-vulnerable.yml Tandoor 1.5.23 (vulnerável) mais PostgreSQL
compose-patched.yml Tandoor 1.5.24 (corrigido) mais PostgreSQL
payloads.txt Os dois payloads de prova de conceito e a saída esperada
enumerate-popen.txt Auxiliar para encontrar o índice de subprocess.Popen em outras imagens
As evidências neste repositório foram capturadas na configuração abaixo. Qualquer versão recente do Docker com Compose v2 ou posterior reproduz o mesmo resultado.
Inicie a stack vulnerável:
docker compose -f compose-vulnerable.yml up -d
Aguarde até que os logs do contêiner web mostrem um worker inicializando, depois abra
http://localhost:8080.
Registre a primeira conta. O primeiro usuário se torna o superusuário. Defina um nome de Space quando solicitado.
Crie uma nova receita. No campo Steps > Instructions, cole o Payload 1 de
payloads.txt, depois salve.
Abra a receita para visualizá-la. As instruções renderizadas mostram
<class 'subprocess.Popen'>, confirmando que a injeção alcança os internos do Python.
Repita com o Payload 2. Visualizar a receita agora executa whoami e exibe root.
Confirme o usuário do processo diretamente:
docker compose -f compose-vulnerable.yml exec web_recipes whoami
Pare a stack vulnerável:
docker compose -f compose-vulnerable.yml down
Inicie a stack corrigida:
docker compose -f compose-patched.yml up -d
Repita os mesmos passos com os mesmos payloads. Em vez de executar, a visualização da receita
agora mostra Could not parse template code. Error: Undefined Error.
Para redefinir tudo e começar de um banco de dados vazio, execute
docker compose -f compose-vulnerable.yml down -v. A flag -v remove os
volumes nomeados, então a próxima execução recria o banco de dados do zero.
O patch (commit e6087d5) substitui o Template() sem sandbox pelo
SandboxedEnvironment do Jinja2:
env = SandboxedEnvironment()
instructions = env.from_string(instructions).render(ingredients=ingredients, scale=scale)
O sandbox sobrescreve a busca de atributos do Jinja2 para proibir o acesso a atributos
inseguros como __class__, __base__, __subclasses__ e __getitem__.
Sem esses, o payload não tem caminho de um objeto simples até subprocess.Popen,
então a expressão falha em vez de executar.
O exploit alcança atributos através do filtro attr() em vez da notação de ponto,
então no código corrigido o acesso bloqueado se manifesta como um UndefinedError
em vez de um SecurityError; ambos são tratados pela versão com sandbox.
Este repositório reproduz uma vulnerabilidade divulgada publicamente e já corrigida para fins educacionais. Execute-o apenas contra os contêineres locais fornecidos aqui. Não ataque sistemas que você não possui ou não está autorizado a testar.