Launchpad

# Default local-file XXE / LFI in `etree.iterparse()` and `ETCompatXMLPar ser()`

## Summary

`lxml` still appears to allow **local-file XXE / local file disclosure by default** through two public entry points:

- `etree. iterparse( ...)`
- `etree. ETCompatXMLPars er()`

Although plain `XMLParser()` has been hardened so that external entity expansion is no longer enabled in the same way by default, these two entry points still appear to reach parsing with external general entity resolution effectively enabled for local files. As a result, attacker-controlled XML can disclose local files such as `file:/ //etc/hostname` during parsing.

## Affected Version

Reproduced on a current local checkout:

- commit: `8096c644bc53`
- reported version string: `7.0.0a0`

## Affected Components

- `src/lxml/ iterparse. pxi`
- `src/lxml/ parser. pxi`

## Reproduction Environment

- Debian trixie based Docker container
- Built and installed from the local source tree
- Python 3.13

## Vulnerability Details

### Impact

If an application parses attacker-controlled XML using either:

- `etree. iterparse( ...)`, or
- `etree. ETCompatXMLPars er()`

an attacker can define an external entity that points to a local file and cause its contents to be expanded into the parsed XML result.

This is not merely theoretical: I reproduced it successfully and observed local file contents being returned through both entry points.

The impact is **local-file XXE / local file disclosure**. `no_network=True` appears to prevent network fetches, but it does **not** prevent local `file://` or equivalent local path resolution.

Successful exploitation allows disclosure of files that are readable by the vulnerable parsing process. The exact impact therefore depends on the runtime environment, including:

- the effective privileges of the process,
- container, chroot, or mount boundaries,
- mounted host paths, and
- additional controls such as SELinux or AppArmor.

### Location

Relevant code paths include:

- `src/lxml/ iterparse. pxi`
  - `iterparse. __init_ _()` around lines `70-127`
- `src/lxml/ parser. pxi`
  - `XMLParser. __init_ _()` around lines `1722-1763`
  - `ETCompatXMLPar ser.__init_ _()` around lines `1799-1838`
  - `_configureSaxC ontext( )` around lines `1018-1028`
  - `_local_resolver()` around lines `472-575`

## PoC / Steps to Reproduce

### PoC script

```python
from io import BytesIO
from lxml import etree

payload = b'''<!DOCTYPE x [<!ENTITY e SYSTEM "file:/ //etc/hostname" >]><x>& e;</x>' ''

for _ev, elem in etree.iterparse (BytesIO( payload) ):
    print( "ITERPARSE: ", repr(elem.text))
    break

parser = etree.ETCompatX MLParser( )
root = etree.fromstrin g(payload, parser=parser)
print("ETCOMPAT:", repr(root.text))
```

### Observed Result

Bboth entry points expanded the entity and returned the contents of `/etc/hostname`:

```text
ITERPARSE: '1e42fc8c57f5\n'
ETCOMPAT: '1e42fc8c57f5\n'
```

This confirms that local file disclosure is reachable through default behavior.

## Expected Behavior

Attacker-controlled XML passed to these public parsing entry points should **not** resolve external entities against local files by default.

At minimum, behavior should be consistent with the hardened `XMLParser()` default.

## Security Classification

- Vulnerability type: **XXE / local file disclosure**
- More specifically: **default local-file XXE through public parser entry points**

Read the original on bugs.launchpad.net ↗