# XPath Injection via Untrusted lxml XPath Expression

Language: Python
Severity: High
CWE: CWE-643

## Source
14

## Flow
14-15-16

## Sink
16

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

app = Flask(__name__)

@app.route('/iot/device/query', methods=['POST'])
def query_device_telemetry():
    device_xml = '''<?xml version="1.0"?>
    <devices>
        <device id="sensor_001" type="temperature" location="warehouse_a" value="22.5" status="active"/>
        <device id="sensor_002" type="humidity" location="warehouse_b" value="65" status="active"/>
        <device id="actuator_003" type="valve" location="warehouse_a" value="closed" status="maintenance"/>
    </devices>'''
    doc = etree.fromstring(device_xml.encode())
    device_filter = request.json.get('filter', 'sensor_001')
    query_expr = f"//device[@id='{device_filter}']"
    results = doc.xpath(query_expr)
    devices = []
    for dev in results:
        devices.append({
            'id': dev.get('id'),
            'type': dev.get('type'),
            'location': dev.get('location'),
            'value': dev.get('value'),
            'status': dev.get('status')
        })
    return jsonify({'devices': devices, 'count': len(devices)})
```

## Explanation

The application constructs an XPath query by directly interpolating user-supplied input from request.json.get('filter') into the XPath expression without sanitization or parameterization. An attacker can inject malicious XPath syntax to bypass intended query logic, extract unauthorized data, or modify query behavior.

## Remediation

The fix validates the user-supplied device filter using a strict allowlist regex that only permits alphanumeric characters, underscores, and hyphens. If the input contains any characters that could be used for XPath injection (such as quotes, parentheses, or brackets), the request is rejected with a 400 error before the XPath query is constructed.

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

app = Flask(__name__)

@app.route('/iot/device/query', methods=['POST'])
def query_device_telemetry():
    device_xml = '''<?xml version="1.0"?>
    <devices>
        <device id="sensor_001" type="temperature" location="warehouse_a" value="22.5" status="active"/>
        <device id="sensor_002" type="humidity" location="warehouse_b" value="65" status="active"/>
        <device id="actuator_003" type="valve" location="warehouse_a" value="closed" status="maintenance"/>
    </devices>'''
    doc = etree.fromstring(device_xml.encode())
    device_filter = request.json.get('filter', 'sensor_001')
    if not re.match(r'^[a-zA-Z0-9_\-]+$', device_filter):
        return jsonify({'error': 'Invalid device filter. Only alphanumeric characters, underscores, and hyphens are allowed.'}), 400
    query_expr = f"//device[@id='{device_filter}']"
    results = doc.xpath(query_expr)
    devices = []
    for dev in results:
        devices.append({
            'id': dev.get('id'),
            'type': dev.get('type'),
            'location': dev.get('location'),
            'value': dev.get('value'),
            'status': dev.get('status')
        })
    return jsonify({'devices': devices, 'count': len(devices)})
```
