#2033

Office validators parse document XML with lxml's default parser (XXE on lxml < 5)

Opengltknkyopened 15d ago

Summary

The Office validators (skills/{docx,pptx,xlsx}/scripts/office/validators/base.py, and pptx.py) parse the XML parts of the document being validated with lxml's default parser:

lxml.etree.parse(str(xml_file))

On lxml < 5.0 the default parser resolves external entities, so a crafted .docx / .pptx / .xlsx makes the validator read local files into the parsed tree. lxml 5.0 changed the default, but the skills don't pin lxml, and the system packages on two current LTS releases are still 4.x: Debian 12 (python3-lxml 4.9.2) and Ubuntu 22.04 (4.8.0).

This is separate from the open XXE work (#58, #657, #1780, #1784, #1837), which covers xml.etree in other files (mcp-builder/scripts/evaluation.py, validators/redlining.py, slack-gif-creator). None of those touch the lxml calls in validators/base.py, and main still has them.

Reproduction

A part containing an external entity, and the same call the validators make:

<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "file:///tmp/canary.txt">]>
<r>&x;</r>
import lxml.etree as e
print(e.tostring(e.parse("part.xml"), encoding="unicode"))
lxmlexternal entityparameter entity (external DTD)entity bomb
4.9.4file contents readfile contents readblocked by libxml2
5.4.0not resolvednot resolvedblocked by libxml2

Suggested fix

Use one hardened parser for every untrusted part:

_SAFE_PARSER = lxml.etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
lxml.etree.parse(str(xml_file), parser=_SAFE_PARSER)

With this parser both entity cases are neutralised on 4.9.4 and 5.4.0, and ordinary parts still parse. OOXML parts don't use DTDs, so nothing legitimate should be lost. The trusted XSD loading can keep its own parser. A lighter alternative is to declare lxml>=5 as a dependency, but the explicit parser doesn't depend on the installed version.

Activity

1
donaldzitzer406-lang15d ago

Summary

The Office validators (skills/{docx,pptx,xlsx}/scripts/office/validators/base.py, and pptx.py) parse the XML parts of the document being validated with lxml's default parser:

lxml.etree.parse(str(xml_file))

On lxml < 5.0 the default parser resolves external entities, so a crafted .docx / .pptx / .xlsx makes the validator read local files into the parsed tree. lxml 5.0 changed the default, but the skills don't pin lxml, and the system packages on two current LTS releases are still 4.x: Debian 12 (python3-lxml 4.9.2) and Ubuntu 22.04 (4.8.0).

This is separate from the open XXE work (#58, #657, #1780, #1784, #1837), which covers xml.etree in other files (mcp-builder/scripts/evaluation.py, validators/redlining.py, slack-gif-creator). None of those touch the lxml calls in validators/base.py, and main still has them.

Reproduction

A part containing an external entity, and the same call the validators make:

<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY x SYSTEM "file:///tmp/canary.txt">]>
<r>&x;</r>
import lxml.etree as e
print(e.tostring(e.parse("part.xml"), encoding="unicode"))
lxmlexternal entityparameter entity (external DTD)entity bomb
4.9.4file contents readfile contents readblocked by libxml2
5.4.0not resolvednot resolvedblocked by libxml2

Suggested fix

Use one hardened parser for every untrusted part:

_SAFE_PARSER = lxml.etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
lxml.etree.parse(str(xml_file), parser=_SAFE_PARSER)

With this parser both entity cases are neutralised on 4.9.4 and 5.4.0, and ordinary parts still parse. OOXML parts don't use DTDs, so nothing legitimate should be lost. The trusted XSD loading can keep its own parser. A lighter alternative is to declare lxml>=5 as a dependency, but the explicit parser doesn't depend on the installed version.