{"title":"XPath Injection via Untrusted lxml XPath Expression","language":"Python","severity":"High","cwe":"CWE-643","source_lines":[8,9],"flow_lines":[8,11,12,9,11,12],"sink_lines":[12],"vulnerable_code":"from lxml import etree\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/iot/device/query', methods=['GET'])\ndef query_device_telemetry():\n    device_id = request.args.get('device_id', '')\n    metric_filter = request.args.get('metric', 'temperature')\n    xml_data = etree.parse('iot_telemetry.xml')\n    xpath_query = f\"//device[@id='{device_id}']/metrics/{metric_filter}\"\n    results = xml_data.xpath(xpath_query)\n    telemetry_values = [elem.text for elem in results]\n    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":"import re\nfrom lxml import etree\nfrom flask import Flask, request, jsonify, abort\n\napp = Flask(__name__)\n\nALLOWED_METRICS = {'temperature', 'humidity', 'pressure', 'light', 'motion', 'voltage', 'current', 'power'}\nDEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_\\-]+$')\n\n@app.route('/iot/device/query', methods=['GET'])\ndef query_device_telemetry():\n    device_id = request.args.get('device_id', '')\n    metric_filter = request.args.get('metric', 'temperature')\n\n    # Validate device_id against a strict allowlist pattern\n    if not device_id or not DEVICE_ID_PATTERN.match(device_id):\n        abort(400, description='Invalid device_id. Only alphanumeric characters, hyphens, and underscores are allowed.')\n\n    # Validate metric against an allowlist of known metrics\n    if metric_filter not in ALLOWED_METRICS:\n        abort(400, description=f'Invalid metric. Allowed metrics: {\", \".join(sorted(ALLOWED_METRICS))}')\n\n    xml_data = etree.parse('iot_telemetry.xml')\n\n    # Use parameterized XPath for device_id to prevent injection\n    # metric_filter is already validated against allowlist so safe to interpolate\n    xpath_query = f\"//device[@id=$device_id]/metrics/{metric_filter}\"\n    results = xml_data.xpath(xpath_query, device_id=device_id)\n\n    telemetry_values = [elem.text for elem in results]\n    return jsonify({'device': device_id, 'data': telemetry_values})"}