# XPath Injection via Untrusted lxml XPath Predicate

Language: Python
Severity: High
CWE: CWE-643

## Source
5-6

## Flow
5-8-9

## Sink
9

## Vulnerable Code
```python
from lxml import etree
from flask import request, jsonify

def query_iot_device_metrics(xml_data):
    device_id = request.args.get('device_id', '')
    metric_type = request.args.get('metric', 'temperature')
    doc = etree.fromstring(xml_data.encode())
    xpath_query = f"//device[@id='{device_id}']/sensors/sensor[@type='{metric_type}']/reading"
    results = doc.xpath(xpath_query)
    metrics = [{'value': r.text, 'timestamp': r.get('timestamp')} for r in results]
    return jsonify({'device': device_id, 'metrics': metrics})
```

## Explanation

User-controlled input from request parameters 'device_id' and 'metric' are directly interpolated into an XPath query string without sanitization. An attacker can inject malicious XPath syntax to bypass intended query logic, extract unauthorized data, or cause denial of service.

## Remediation

The fix uses lxml's built-in XPath parameterization (XPath variables via $variable_name syntax) which safely passes user input as string parameters rather than interpolating them into the query string, completely preventing XPath injection. Additionally, a whitelist validation function rejects any input containing characters outside of alphanumerics, hyphens, underscores, and dots as a defense-in-depth measure.

## Secure Code
```python
import re
from lxml import etree
from flask import request, jsonify, abort

def _validate_identifier(value, field_name):
    """Validate that the input is a safe identifier (alphanumeric, hyphens, underscores, dots)."""
    if not value:
        abort(400, description=f"Missing required parameter: {field_name}")
    if not re.match(r'^[a-zA-Z0-9_\-\.]+$', value):
        abort(400, description=f"Invalid characters in parameter: {field_name}")
    return value

def query_iot_device_metrics(xml_data):
    device_id = request.args.get('device_id', '')
    metric_type = request.args.get('metric', 'temperature')
    
    # Validate inputs to prevent XPath injection
    device_id = _validate_identifier(device_id, 'device_id')
    metric_type = _validate_identifier(metric_type, 'metric')
    
    doc = etree.fromstring(xml_data.encode())
    
    # Use parameterized XPath via XPath class with variables to prevent injection
    xpath_expr = etree.XPath(
        "//device[@id=$device_id]/sensors/sensor[@type=$metric_type]/reading"
    )
    results = xpath_expr(doc, device_id=device_id, metric_type=metric_type)
    
    metrics = [{'value': r.text, 'timestamp': r.get('timestamp')} for r in results]
    return jsonify({'device': device_id, 'metrics': metrics})
```
