Injeção de entidade externa XML (XXE, CWE-611): como funciona e como evitar
Um parser XML que respeita o DTD de um documento busca entidades SYSTEM em URLs file:// ou http:// e cola o resultado nos dados lidos, então um documento enviado pode ler arquivos do servidor ou fazer o servidor chamar endereços internos. Como funcionam entidades externas e a expansão de entidades, quais configurações do parser as causam e como o defusedxml com forbid_dtd=True as bloqueia, com um exemplo em Flask.
Explicação em Linguagem Simples (ELI5)
Uma gráfica preenche cartas-modelo. A carta de um cliente diz: 'Prezado [insira aqui o conteúdo da gaveta 4 do seu arquivo]'. Um impressor cuidadoso só insere palavras que o cliente forneceu. Um impressor descuidado vai até a gaveta 4, que guarda contratos de outros clientes, e os imprime na carta que devolve pelo correio. O XXE é o impressor descuidado: o documento XML manda o parser buscar um arquivo ou um endereço web e inseri-lo, e o parser obedece. A correção é recusar qualquer carta que traga instruções do tipo 'insira de outro lugar'.
Conceitos Centrais e Termos
- DTD e entidades
- Uma Document Type Definition em
<!DOCTYPE ...>pode declarar entidades, trechos de texto nomeados que o parser substitui onde aparecer&nome;. Entidades internas guardam texto literal; as externas apontam para outro lugar. - Entidade externa
<!ENTITY x SYSTEM "file:///etc/hostname">manda o parser ler essa URL e colocar o conteúdo onde&x;for usado. Com URLshttp://quem faz a requisição é o servidor, o que transforma o XXE em falsificação de requisição do lado do servidor.- Expansão de entidades (Billion Laughs)
- Entidades que referenciam outras entidades dez vezes seguidas transformam um documento de um kilobyte em gigabytes de texto na memória (CWE-776). Isso não precisa de acesso externo, então só rejeitar DTDs ou limitar a expansão resolve.
- Padrões do parser
- O comportamento depende da biblioteca: o lxml 5 resolve só entidades internas por padrão, mas
resolve_entities=Trueeload_dtd=Truereabrem a brecha; oDocumentBuilderFactorydo Java resolve entidades externas a menos quedisallow-doctype-declesteja ativo; e o .NET precisa deDtdProcessing.Prohibit. - defusedxml
- Um pacote Python que envolve os parsers padrão e lança
EntitiesForbidden,ExternalReferenceForbiddenouDTDForbiddenem vez de processar construções perigosas.forbid_dtd=Truerejeita qualquer documento com DOCTYPE.
Fluxo de Ataque Passo a Paso
O atacante envia um documento XML
Ele declara uma entidade externa e a referencia num campo que o servidor devolve. Uma entidade apontando para file:///etc/hostname ilustra o caso de leitura.
O parser processa o DTD
Como a aplicação ativou o carregamento de DTD e a resolução de entidades, o parser lê o arquivo ou a URL referenciada.
O conteúdo do arquivo vira dado
O texto cai no elemento customer, e o endpoint o devolve na resposta JSON.
O atacante lê arquivos ou alcança hosts internos
Arquivos de configuração, chaves e metadados da nuvem podem ser lidos assim. Em 2014 Reginaldo Silva recebeu do Facebook uma recompensa de 33.500 dólares por um XXE no tratamento de OpenID que permitia ler arquivos nos servidores.
Código-Fonte: Vulnerável vs. Seguro
# invoices.py: o parser segue o que o DTD enviado pedir
from flask import Flask, request
from lxml import etree
app = Flask(__name__)
# entidades resolvidas, DTDs carregados e acesso à rede permitido
@app.post("/invoices/import")
def import_invoice():
parser = etree.XMLParser(resolve_entities=True, load_dtd=True, no_network=False)
root = etree.fromstring(request.get_data(), parser)
return {"customer": root.findtext("customer"), "total": root.findtext("total")}
# invoices.py: documentos com DTD são rejeitados antes do parse
from defusedxml import DefusedXmlException
from defusedxml.ElementTree import ParseError, fromstring
from flask import Flask, abort, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1_000_000
# sem DOCTYPE, sem entidades, sem referências externas; corpo limitado a 1 MB
@app.post("/invoices/import")
def import_invoice():
try:
root = fromstring(request.get_data(), forbid_dtd=True)
except (DefusedXmlException, ParseError):
abort(400)
return {"customer": root.findtext("customer"), "total": root.findtext("total")}
Lista de Verificação de Segurança para Engenharia
- Faça o parse de XML não confiável com
defusedxmlem Python, ou com DTDs desativados nas outras linguagens:disallow-doctype-declcomo true no Java,DtdProcessing.Prohibitno .NET, libxml2 semXML_PARSE_NOENTeXML_PARSE_DTDLOAD. - Nunca passe
resolve_entities=True,load_dtd=Trueouno_network=Falseao lxml para entradas que você não controla, e rode o Bandit (B320 sinalizalxml.etree) no CI. - Limite o tamanho da requisição (
MAX_CONTENT_LENGTH) e rejeite qualquer documento que contenha DOCTYPE. - Trate arquivos SVG, SAML, DOCX e XLSX enviados como XML e processe-os com as mesmas configurações reforçadas.
- Adicione um teste no CI que envie um documento com uma entidade externa apontando para
file:///etc/hostnamee espere 400.