# XPath Injection via lxml.etree.XPath() on Untrusted Input

Language: Python
Severity: High
CWE: CWE-643

## Source
8-9

## Flow
8-10-11-12

## Sink
11-12

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

app = flask.Flask(__name__)

@app.route('/iot/device_telemetry')
def fetch_device_metrics():
    device_id = flask.request.args.get('device_id', '')
    sensor_type = flask.request.args.get('sensor', 'temperature')
    xml_data = etree.parse('iot_telemetry.xml')
    query = f"//device[@id='{device_id}']/sensors/{sensor_type}/reading"
    xpath_eval = etree.XPath(query)
    results = xpath_eval(xml_data)
    return flask.jsonify({'readings': [r.text for r in results]})
```

## Explanation

User-supplied input from 'device_id' and 'sensor' parameters are directly interpolated into an XPath query string without sanitization or parameterization. An attacker can inject malicious XPath expressions to bypass filters, access unauthorized data, or extract the entire XML structure.

## Remediation

The fix uses XPath parameterized variables ($device_id, $sensor_type) via lxml's XPath variable binding mechanism, which prevents injection by treating inputs as literal string values rather than XPath syntax. Additionally, strict input validation is applied: device_id is validated against an alphanumeric pattern, and sensor_type is checked against an allowlist of known sensor types, providing defense-in-depth.

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

app = flask.Flask(__name__)

ALLOWED_SENSOR_TYPES = {'temperature', 'humidity', 'pressure', 'voltage', 'current', 'power', 'light', 'motion'}
DEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_\-]+$')


def sanitize_device_id(device_id):
    """Validate device_id contains only safe characters."""
    if not device_id or not DEVICE_ID_PATTERN.match(device_id):
        return None
    return device_id


def sanitize_sensor_type(sensor_type):
    """Validate sensor_type against an allowlist."""
    if sensor_type in ALLOWED_SENSOR_TYPES:
        return sensor_type
    return None


@app.route('/iot/device_telemetry')
def fetch_device_metrics():
    device_id = flask.request.args.get('device_id', '')
    sensor_type = flask.request.args.get('sensor', 'temperature')

    safe_device_id = sanitize_device_id(device_id)
    if safe_device_id is None:
        return flask.jsonify({'error': 'Invalid device_id format'}), 400

    safe_sensor_type = sanitize_sensor_type(sensor_type)
    if safe_sensor_type is None:
        return flask.jsonify({'error': 'Invalid sensor type'}), 400

    xml_data = etree.parse('iot_telemetry.xml')

    # Use XPath variables for parameterized query to prevent injection
    query = etree.XPath(
        "//device[@id=$device_id]/sensors/*[local-name()=$sensor_type]/reading"
    )
    results = query(xml_data, device_id=safe_device_id, sensor_type=safe_sensor_type)
    return flask.jsonify({'readings': [r.text for r in results]})
```
