{"title":"XPath Injection via lxml.etree.XPath() on Untrusted Input","language":"Python","severity":"High","cwe":"CWE-643","source_lines":[8,9],"flow_lines":[8,10,11,12],"sink_lines":[11,12],"vulnerable_code":"from lxml import etree\nimport flask\n\napp = flask.Flask(__name__)\n\n@app.route('/iot/device_telemetry')\ndef fetch_device_metrics():\n    device_id = flask.request.args.get('device_id', '')\n    sensor_type = flask.request.args.get('sensor', 'temperature')\n    xml_data = etree.parse('iot_telemetry.xml')\n    query = f\"//device[@id='{device_id}']/sensors/{sensor_type}/reading\"\n    xpath_eval = etree.XPath(query)\n    results = xpath_eval(xml_data)\n    return flask.jsonify({'readings': [r.text for r in results]})","explanation":"User-supplied input from 'device_id' and 'sensor' parameters are directly interpolated into an XPath query string without sanitization or parameterization. An attacker can inject malicious XPath expressions to bypass filters, access unauthorized data, or extract the entire XML structure.","remediation":"The fix uses XPath parameterized variables ($device_id, $sensor_type) via lxml's XPath variable binding mechanism, which prevents injection by treating inputs as literal string values rather than XPath syntax. Additionally, strict input validation is applied: device_id is validated against an alphanumeric pattern, and sensor_type is checked against an allowlist of known sensor types, providing defense-in-depth.","secure_code":"from lxml import etree\nimport flask\nimport re\n\napp = flask.Flask(__name__)\n\nALLOWED_SENSOR_TYPES = {'temperature', 'humidity', 'pressure', 'voltage', 'current', 'power', 'light', 'motion'}\nDEVICE_ID_PATTERN = re.compile(r'^[a-zA-Z0-9_\\-]+$')\n\n\ndef sanitize_device_id(device_id):\n    \"\"\"Validate device_id contains only safe characters.\"\"\"\n    if not device_id or not DEVICE_ID_PATTERN.match(device_id):\n        return None\n    return device_id\n\n\ndef sanitize_sensor_type(sensor_type):\n    \"\"\"Validate sensor_type against an allowlist.\"\"\"\n    if sensor_type in ALLOWED_SENSOR_TYPES:\n        return sensor_type\n    return None\n\n\n@app.route('/iot/device_telemetry')\ndef fetch_device_metrics():\n    device_id = flask.request.args.get('device_id', '')\n    sensor_type = flask.request.args.get('sensor', 'temperature')\n\n    safe_device_id = sanitize_device_id(device_id)\n    if safe_device_id is None:\n        return flask.jsonify({'error': 'Invalid device_id format'}), 400\n\n    safe_sensor_type = sanitize_sensor_type(sensor_type)\n    if safe_sensor_type is None:\n        return flask.jsonify({'error': 'Invalid sensor type'}), 400\n\n    xml_data = etree.parse('iot_telemetry.xml')\n\n    # Use XPath variables for parameterized query to prevent injection\n    query = etree.XPath(\n        \"//device[@id=$device_id]/sensors/*[local-name()=$sensor_type]/reading\"\n    )\n    results = query(xml_data, device_id=safe_device_id, sensor_type=safe_sensor_type)\n    return flask.jsonify({'readings': [r.text for r in results]})"}