# XPath Injection via Untrusted lxml XPath Expression

Language: Python
Severity: High
CWE: CWE-643

## Source
8, 9

## Flow
8-11-12, 9-11-12

## Sink
12

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

app = Flask(__name__)

@app.route('/iot/device/query', methods=['GET'])
def query_device_telemetry():
    device_id = request.args.get('device_id', '')
    metric_filter = request.args.get('metric', 'temperature')
    xml_data = etree.parse('iot_telemetry.xml')
    xpath_query = f"//device[@id='{device_id}']/metrics/{metric_filter}"
    results = xml_data.xpath(xpath_query)
    telemetry_values = [elem.text for elem in results]
    return jsonify({'device': device_id, 'data': telemetry_values})
```

## Explanation

The application constructs an XPath query using unsanitized user input from 'device_id' and 'metric' parameters via string formatting. An attacker can inject malicious XPath syntax to bypass intended query logic, extract unauthorized data, or manipulate query results.

## Remediation

The fix uses lxml's built-in XPath parameterization (variable binding via $device_id) to safely pass the device_id value without string interpolation, preventing XPath injection through that parameter. The metric_filter is validated against a strict allowlist of known metric names, ensuring only predefined safe values can be used in the query. Additionally, device_id is validated with a regex pattern as a defense-in-depth measure.

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

app = Flask(__name__)

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

@app.route('/iot/device/query', methods=['GET'])
def query_device_telemetry():
    device_id = request.args.get('device_id', '')
    metric_filter = request.args.get('metric', 'temperature')

    # Validate device_id against a strict allowlist pattern
    if not device_id or not DEVICE_ID_PATTERN.match(device_id):
        abort(400, description='Invalid device_id. Only alphanumeric characters, hyphens, and underscores are allowed.')

    # Validate metric against an allowlist of known metrics
    if metric_filter not in ALLOWED_METRICS:
        abort(400, description=f'Invalid metric. Allowed metrics: {", ".join(sorted(ALLOWED_METRICS))}')

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

    # Use parameterized XPath for device_id to prevent injection
    # metric_filter is already validated against allowlist so safe to interpolate
    xpath_query = f"//device[@id=$device_id]/metrics/{metric_filter}"
    results = xml_data.xpath(xpath_query, device_id=device_id)

    telemetry_values = [elem.text for elem in results]
    return jsonify({'device': device_id, 'data': telemetry_values})
```
