● CWE-611 · OWASP A05:2021

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 URLs http:// 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=True e load_dtd=True reabrem a brecha; o DocumentBuilderFactory do Java resolve entidades externas a menos que disallow-doctype-decl esteja ativo; e o .NET precisa de DtdProcessing.Prohibit.
defusedxml
Um pacote Python que envolve os parsers padrão e lança EntitiesForbidden, ExternalReferenceForbidden ou DTDForbidden em vez de processar construções perigosas. forbid_dtd=True rejeita qualquer documento com DOCTYPE.

Fluxo de Ataque Passo a Paso

Passo 1

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.

Passo 2

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.

Passo 3

O conteúdo do arquivo vira dado

O texto cai no elemento customer, e o endpoint o devolve na resposta JSON.

Passo 4

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

IMPLEMENTAÇÃO VULNERÁVEL
# 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")}
PATCH SEGURO E ROBUSTO
# 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

Fontes

← Ver o diretório completo de segurança Todos os guias de vulnerabilidades →