{"title":"XPath Injection via Untrusted lxml XPath Expression","language":"Python","severity":"High","cwe":"CWE-643","source_lines":[14],"flow_lines":[14,15,16],"sink_lines":[16],"vulnerable_code":"from lxml import etree\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/iot/device/query', methods=['POST'])\ndef query_device_telemetry():\n    device_xml = '''<?xml version=\"1.0\"?>\n    <devices>\n        <device id=\"sensor_001\" type=\"temperature\" location=\"warehouse_a\" value=\"22.5\" status=\"active\"/>\n        <device id=\"sensor_002\" type=\"humidity\" location=\"warehouse_b\" value=\"65\" status=\"active\"/>\n        <device id=\"actuator_003\" type=\"valve\" location=\"warehouse_a\" value=\"closed\" status=\"maintenance\"/>\n    </devices>'''\n    doc = etree.fromstring(device_xml.encode())\n    device_filter = request.json.get('filter', 'sensor_001')\n    query_expr = f\"//device[@id='{device_filter}']\"\n    results = doc.xpath(query_expr)\n    devices = []\n    for dev in results:\n        devices.append({\n            'id': dev.get('id'),\n            'type': dev.get('type'),\n            'location': dev.get('location'),\n            'value': dev.get('value'),\n            'status': dev.get('status')\n        })\n    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":"from lxml import etree\nfrom flask import Flask, request, jsonify\nimport re\n\napp = Flask(__name__)\n\n@app.route('/iot/device/query', methods=['POST'])\ndef query_device_telemetry():\n    device_xml = '''<?xml version=\"1.0\"?>\n    <devices>\n        <device id=\"sensor_001\" type=\"temperature\" location=\"warehouse_a\" value=\"22.5\" status=\"active\"/>\n        <device id=\"sensor_002\" type=\"humidity\" location=\"warehouse_b\" value=\"65\" status=\"active\"/>\n        <device id=\"actuator_003\" type=\"valve\" location=\"warehouse_a\" value=\"closed\" status=\"maintenance\"/>\n    </devices>'''\n    doc = etree.fromstring(device_xml.encode())\n    device_filter = request.json.get('filter', 'sensor_001')\n    if not re.match(r'^[a-zA-Z0-9_\\-]+$', device_filter):\n        return jsonify({'error': 'Invalid device filter. Only alphanumeric characters, underscores, and hyphens are allowed.'}), 400\n    query_expr = f\"//device[@id='{device_filter}']\"\n    results = doc.xpath(query_expr)\n    devices = []\n    for dev in results:\n        devices.append({\n            'id': dev.get('id'),\n            'type': dev.get('type'),\n            'location': dev.get('location'),\n            'value': dev.get('value'),\n            'status': dev.get('status')\n        })\n    return jsonify({'devices': devices, 'count': len(devices)})"}