# XPath Injection via lxml.etree.XPath Predicate Construction

Language: Python
Severity: High
CWE: CWE-643

## Source
3

## Flow
3-4-5

## Sink
4-5

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

def retrieve_iot_device_metrics(device_xml, sensor_id):
    doc = etree.fromstring(device_xml)
    query = f"//device/sensor[@id='{sensor_id}']/metrics"
    xpath_eval = etree.XPath(query)
    results = xpath_eval(doc)
    if results:
        return etree.tostring(results[0], encoding='unicode')
    return None
```

## Explanation

The sensor_id parameter is directly interpolated into an XPath query string using an f-string without sanitization. An attacker can inject malicious XPath expressions through sensor_id to bypass access controls, extract unauthorized data, or manipulate query logic.

## Remediation

The fix uses lxml's built-in XPath variable substitution via the `$variable` syntax and passing the parameter as a keyword argument to `doc.xpath()`, which safely handles the value without allowing injection. Additionally, an input validation whitelist ensures that only expected characters are accepted in the sensor_id parameter, providing defense in depth.

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

def retrieve_iot_device_metrics(device_xml, sensor_id):
    # Validate sensor_id to only allow safe characters (alphanumeric, hyphens, underscores)
    if not re.match(r'^[a-zA-Z0-9_\-]+$', sensor_id):
        raise ValueError("Invalid sensor_id: only alphanumeric characters, hyphens, and underscores are allowed")
    doc = etree.fromstring(device_xml)
    query = "//device/sensor[@id=$sensor_id]/metrics"
    results = doc.xpath(query, sensor_id=sensor_id)
    if results:
        return etree.tostring(results[0], encoding='unicode')
    return None
```
