{"title":"SQL Injection via SQLAlchemy Raw Query with String Formatting","language":"Python","severity":"Critical","cwe":"CWE-89","source_lines":[7,8],"flow_lines":[7,8,10,11],"sink_lines":[11],"vulnerable_code":"from sqlalchemy import create_engine, text\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\nengine = create_engine('postgresql://iot_admin:pass@localhost/sensor_db')\n@app.route('/api/sensors/telemetry', methods=['GET'])\ndef fetch_sensor_telemetry():\n    device_id = request.args.get('device_id')\n    metric_type = request.args.get('metric')\n    with engine.connect() as conn:\n        raw_qry = f\"SELECT ts, payload FROM telemetry_logs WHERE device_id='{device_id}' AND metric='{metric_type}' ORDER BY ts DESC LIMIT 50\"\n        rows = conn.execute(text(raw_qry))\n        return jsonify([dict(r) for r in rows])","explanation":"User-controlled inputs `device_id` and `metric_type` are obtained directly from HTTP query parameters without any sanitization or parameterization. These values are interpolated directly into a raw SQL query string using an f-string on line 10, and then executed via `conn.execute(text(raw_qry))` on line 11. Despite using SQLAlchemy's `text()` wrapper, no bound parameters are used, meaning the `text()` call provides zero protection against SQL injection.","remediation":"The fix replaces the dangerous f-string interpolation with SQLAlchemy's parameterized query mechanism using named bind parameters (`:device_id` and `:metric_type`) within the `text()` construct, and passes the user inputs as a separate dictionary to `conn.execute()`. This ensures the database driver properly escapes and handles the values as data rather than executable SQL, completely preventing SQL injection. Additionally, input presence validation was added to reject requests missing required parameters.","secure_code":"from sqlalchemy import create_engine, text\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\nengine = create_engine('postgresql://iot_admin:pass@localhost/sensor_db')\n@app.route('/api/sensors/telemetry', methods=['GET'])\ndef fetch_sensor_telemetry():\n    device_id = request.args.get('device_id')\n    metric_type = request.args.get('metric')\n    if not device_id or not metric_type:\n        return jsonify({\"error\": \"device_id and metric parameters are required\"}), 400\n    with engine.connect() as conn:\n        raw_qry = text(\"SELECT ts, payload FROM telemetry_logs WHERE device_id=:device_id AND metric=:metric_type ORDER BY ts DESC LIMIT 50\")\n        rows = conn.execute(raw_qry, {\"device_id\": device_id, \"metric_type\": metric_type})\n        return jsonify([dict(r._mapping) for r in rows])"}