fix(security): bump lxml para 6.1.1, corrige XXE de leitura de arquivo local (fase 4, #1251) - #1283
Open
Rossi-Luciano wants to merge 1 commit into
Open
Conversation
…o local (fase 4, scieloorg#1251) Fase 4 da estrategia de atualizacao de dependencias, com prioridade elevada por envolver uma vulnerabilidade ativa, nao apenas uma CVE rotineira. ## Vulnerabilidade corrigida (PYSEC-2026-87) lxml < 6.1.0 resolve entidades externas por padrao (resolve_entities=True), permitindo que um XML nao confiavel leia arquivos locais via <!ENTITY xxe SYSTEM "file://...">. Confirmado empiricamente neste repositorio: um XML malicioso processado por packtools.XML() (o parser principal, load_dtd=True, no_network=True) conseguia vazar o conteudo de um arquivo local arbitrario. no_network so bloqueia esquemas de rede (http/ftp), nao file://. Como packtools processa pacotes XML de terceiros por definicao, isso e um vetor de exfiltracao de arquivo local ativo, nao teorico. ## Correcoes aplicadas 1. lxml 4.9.3 -> 6.1.1 em requirements.txt (muda o default para resolve_entities='internal', a partir da 6.1.0). 2. Endurecimento explicito: resolve_entities="internal" adicionado nos 5 pontos onde XMLParser() e instanciado (packtools/utils.py, packtools/sps/pid_provider/xml_loader.py, packtools/sps/utils/xml_utils.py), para nao depender apenas do default da biblioteca. 'internal' (nao False) foi escolhido porque preserva a resolucao de entidades internas (ex.: entidades de caracteres definidas inline no DOCTYPE), testado e confirmado nao quebrar nenhum caso legitimo; resolve_entities=False bloquearia isso tambem. 3. setup.py: piso de 'lxml>=4.9.2' para 'lxml>=6.1.0', excecao pontual a regra geral de deixar setup.py para a fase 8, justificada pela gravidade: manter o piso solto deixaria `pip install packtools` (sem passar por requirements.txt) potencialmente vulneravel mesmo apos este fix. 4. tox.ini: fator lxml492 -> lxml611, para nao testar mais deliberadamente contra uma versao do lxml com CVE conhecida aberta. ## Regressao real encontrada e corrigida (nao relacionada a seguranca) Rodar a suite completa com lxml 6.1.1 sozinho (sem o endurecimento acima) revelou 15 falhas novas em packtools/sps/utils/xml_utils.py e packtools/sps/models/test_article_titles.py, alem do flake ja conhecido de test_i18n.py. Causa raiz isolada com reproducao minima comparando lxml 4.9.3 vs 6.1.1: Element.addnext() deixou de mover automaticamente o .tail do elemento de origem para o elemento inserido. O workaround existente em process_xref() (marcador EMPTYTAGTOKEEPXREFTAIL, usado para preservar o texto apos um <xref> antes de parent.remove(xref) descarta-lo) dependia desse efeito colateral implicito do lxml 4.x para funcionar. Corrigido movendo o tail explicitamente (e.tail = xref.tail; xref.tail = None) antes do addnext(), o que funciona identicamente em ambas as versoes (validado nas duas). ## Validacao pytest completo (venv isolado, so lxml + os 3 arquivos de codigo alterados, resto identico ao master): apos o fix do process_xref, duas rodadas deram 40 failed / 5973 passed / 30 skipped, identico ao baseline do master (a primeira rodada, antes do fix, mostrou 55 failed, confirmando a regressao real acima). tox -e py310-lxml611: 23 failures + 19 errors, numeros identicos aos ja documentados na fase 1 (PR scieloorg#1280) e causados pelo mesmo gap pre-existente (Pillow sem pin em setup.py resolvendo para uma versao mais nova via bounds soltos) -- nao relacionado a este bump de lxml.
16 tasks
16 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
O que esse PR faz?
Fase 4 da estratégia de atualização de dependências definida na issue #1251:
lxml4.9.3 → 6.1.1. Prioridade elevada em relação às fases anteriores: não é só uma CVE rotineira, é uma vulnerabilidade ativa neste repositório.Vulnerabilidade corrigida (PYSEC-2026-87)
lxml < 6.1.0resolve entidades externas por padrão (resolve_entities=True), permitindo que um XML não confiável leia arquivos locais via<!ENTITY xxe SYSTEM "file://...">.Confirmei empiricamente que este repositório está exposto: um XML malicioso processado por
packtools.XML()(o parser principal, comload_dtd=True,no_network=True) conseguia vazar o conteúdo de um arquivo local arbitrário.no_networksó bloqueia esquemas de rede (http/ftp), nãofile://. Como o packtools processa pacotes XML de terceiros por definição, esse é um vetor de exfiltração de arquivo local ativo, não teórico.Correções aplicadas
lxml4.9.3 → 6.1.1 emrequirements.txt(muda o default pararesolve_entities='internal'a partir da 6.1.0).resolve_entities="internal"adicionado nos 5 pontos ondeXMLParser()é instanciado (packtools/utils.py,packtools/sps/pid_provider/xml_loader.py,packtools/sps/utils/xml_utils.py), para não depender só do default da biblioteca. Escolhi'internal'(nãoFalse) porque preserva a resolução de entidades internas (ex.: entidades de caractere definidas inline no DOCTYPE) e bloqueia só as externas: testei e confirmei queresolve_entities=Falsequebraria entidades internas legítimas.setup.py: piso delxml>=4.9.2paralxml>=6.1.0. Isso é uma exceção pontual à regra geral de deixarsetup.pypara a fase 8 (sincronizar bounds), justificada pela gravidade: manter o piso solto deixariapip install packtools(sem passar porrequirements.txt) potencialmente vulnerável mesmo depois deste fix.tox.ini: fatorlxml492→lxml611, para não testar mais deliberadamente contra uma versão do lxml com CVE conhecida em aberto.Regressão real encontrada e corrigida (não relacionada a segurança)
Rodar a suíte completa com
lxml6.1.1 sozinho (sem o endurecimento acima) revelou 15 falhas novas empacktools/sps/utils/xml_utils.pyetest_article_titles.py, além do flake já conhecido detest_i18n.py.Isolei a causa com uma reprodução mínima comparando lxml 4.9.3 vs 6.1.1:
Element.addnext()deixou de mover automaticamente o.taildo elemento de origem para o elemento inserido. O workaround existente emprocess_xref()(marcadorEMPTYTAGTOKEEPXREFTAIL, usado para preservar o texto após um<xref>antes deparent.remove(xref)descartá-lo) dependia desse efeito colateral implícito do lxml 4.x para funcionar. Corrigi movendo otailexplicitamente (e.tail = xref.tail; xref.tail = None) antes doaddnext(), o que funciona identicamente em ambas as versões (validei nas duas).Onde a revisão poderia começar?
packtools/sps/utils/xml_utils.py, funçãoprocess_xref(): o fix da regressão detail.resolve_entities="internal"novo.setup.py/tox.ini: mudança de piso, fora do escopo usual desta fase, mas justificada acima.Como este poderia ser testado manualmente?
Antes deste PR (lxml 4.9.3): retorna o conteúdo do arquivo local. Depois (lxml 6.1.1 +
resolve_entities="internal"): levantaXMLSyntaxError: Entity 'xxe' not defined.pytest -qdeve dar o mesmo resultado do master (40 failed pré-existentes, 5973 passed, 30 skipped).Algum cenário de contexto que queira dar?
Independente das fases 1-3 (PR #1280/#1281/#1282): parte direto do master.
Validação:
pytestcompleto com venv isolado (só lxml + os 3 arquivos de código alterados, resto igual ao master). Antes do fix doprocess_xref: 55 failed (a regressão real). Depois: duas rodadas deram 40 failed / 5973 passed / 30 skipped, idêntico ao baseline.Também rodei
tox -e py310-lxml611(único Python 3.9/3.10/3.11 disponível localmente): 23 failures + 19 errors, sem relação com este bump de lxml. Correção (2026-08-16, adicionada após a fase 8/#1287): originalmente atribuí esse resultado a "Pillow sem pin em setup.py", isso estava errado. Investigando a fundo na fase 8, descobri que são exatamente as mesmas 40 falhas pré-existentes da issue #1276 (18 dohtmlgenerator+ 22 outras), só contadas de forma diferente pelounittest, mais 2ERRORespúrios causados por um bug de descoberta de testes emtox.ini(python -m unittest -vvvtentando importarpacktools/sps/sps_versions/sps-1.9/sps-1.10como módulo de teste). Ambos corrigidos no PR #1287.Screenshots
N/A (mudança de dependência + parsing XML, sem interface).
Quais são os tickets relevantes?
Parte de #1251 (fase 4 da estratégia de atualização de dependências). Corrige PYSEC-2026-87.
Referências
Estratégia priorizada descrita nos comentários de progresso da issue #1251. Advisory: https://github.com/lxml/lxml/security/advisories (PYSEC-2026-87, GHSA correspondente).
Segurança da informação (NSI.04)
Este PR manipula dados sensíveis ou pessoais (LGPD)?
Este PR altera autenticação, autorização, controle de acesso ou gerenciamento de sessão?
Este PR introduz, atualiza ou remove dependências de terceiros?
Sim — as novas dependências foram verificadas no SBOM/Trivy sem vulnerabilidades críticas/altas em aberto?
Este repositório não usa Trivy/SBOM (é biblioteca, não serviço containerizado, conforme
SECURITY_ADHERENCE.mdseção 3). O gate real de dependências é o Snyk, que roda automaticamente neste PR. Verifiquei manualmente compip-audite confirmei empiricamente (ver seção acima) que a versão-alvo corrige a CVE PYSEC-2026-87 ativa nesta versão do lxml.Não
Este PR foi validado pelo pipeline de segurança (SonarQube / Trivy)?
SECURITY_ADHERENCE.mdseção 3). Os gates automáticos reais (Snyk e GitGuardian) rodam neste PR.Este PR concatena, monta ou executa comandos SQL, HTML ou JavaScript a partir de entrada externa?
Este PR expõe novos endpoints, telas ou serviços?
Algum segredo, senha, chave ou token está sendo adicionado ao código-fonte?