# XML Entity Expansion (Billion Laughs) DoS via lxml XMLParser(resolve_entities=True)

Language: Python
Severity: High
CWE: CWE-776

## Source
4

## Flow
4-6-7-8

## Sink
8

## Vulnerable Code
```python
from lxml import etree
import io

def process_iot_device_telemetry(xml_payload):
    parser = etree.XMLParser(resolve_entities=True, no_network=False)
    try:
        telemetry_stream = io.BytesIO(xml_payload.encode('utf-8'))
        device_tree = etree.parse(telemetry_stream, parser)
        root = device_tree.getroot()
        device_id = root.find('.//deviceId').text
        sensor_data = {}
        for metric in root.findall('.//metric'):
            sensor_data[metric.get('name')] = metric.text
        return {'device': device_id, 'readings': sensor_data, 'status': 'processed'}
    except Exception as e:
        return {'error': str(e), 'status': 'failed'}

def ingest_telemetry_batch(devices_xml):
    results = []
    for device_xml in devices_xml:
        result = process_iot_device_telemetry(device_xml)
        results.append(result)
    return results
```

## Explanation

The function accepts untrusted XML input (xml_payload) and parses it with resolve_entities=True, which enables external entity processing. This allows attackers to craft malicious XML with recursive entity definitions (Billion Laughs attack) or external entity references that can cause denial of service through exponential memory/CPU consumption or data exfiltration.

## Remediation

The fix disables entity resolution by setting resolve_entities=False, prevents network access with no_network=True, and disables DTD loading/validation with dtd_validation=False and load_dtd=False. Additionally, huge_tree=False is set to enforce default size limits, preventing memory exhaustion from excessively large documents. These settings together prevent both Billion Laughs (entity expansion) attacks and XXE (external entity) attacks.

## Secure Code
```python
from lxml import etree
import io

def process_iot_device_telemetry(xml_payload):
    parser = etree.XMLParser(resolve_entities=False, no_network=True, dtd_validation=False, load_dtd=False, huge_tree=False)
    try:
        telemetry_stream = io.BytesIO(xml_payload.encode('utf-8'))
        device_tree = etree.parse(telemetry_stream, parser)
        root = device_tree.getroot()
        device_id = root.find('.//deviceId').text
        sensor_data = {}
        for metric in root.findall('.//metric'):
            sensor_data[metric.get('name')] = metric.text
        return {'device': device_id, 'readings': sensor_data, 'status': 'processed'}
    except Exception as e:
        return {'error': str(e), 'status': 'failed'}

def ingest_telemetry_batch(devices_xml):
    results = []
    for device_xml in devices_xml:
        result = process_iot_device_telemetry(device_xml)
        results.append(result)
    return results
```
