{"title":"SQL Injection via f-string concatenation in sqlite3 queries","language":"Python","severity":"Critical","cwe":"CWE-89","source_lines":[8,9,10],"flow_lines":[8,13,9,13,10,13,13,14],"sink_lines":[14],"vulnerable_code":"import sqlite3\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n@app.route('/iot/device/telemetry', methods=['GET'])\ndef fetch_device_telemetry():\n    device_mac = request.args.get('mac_address', '')\n    time_range = request.args.get('hours', '24')\n    metric_type = request.args.get('metric', 'temperature')\n    conn = sqlite3.connect('/var/iot/telemetry.db')\n    cursor = conn.cursor()\n    query = f\"SELECT timestamp, {metric_type}, device_id FROM sensor_data WHERE mac_address = '{device_mac}' AND timestamp > datetime('now', '-{time_range} hours') ORDER BY timestamp DESC\"\n    cursor.execute(query)\n    results = cursor.fetchall()\n    telemetry_data = []\n    for row in results:\n        telemetry_data.append({\n            'timestamp': row[0],\n            'value': row[1],\n            'device': row[2]\n        })\n    conn.close()\n    return jsonify({'status': 'success', 'data': telemetry_data})","explanation":"The code constructs an SQL query using f-string formatting with three unsanitized user inputs (device_mac, time_range, metric_type) directly concatenated into the query string. The cursor.execute() function then executes this dynamically constructed query without parameterization, allowing attackers to inject arbitrary SQL commands through any of these parameters.","remediation":"The fix addresses all three injection vectors: metric_type is validated against a strict allowlist of permitted column names, time_range is validated as a positive integer (eliminating string injection), and device_mac is passed as a parameterized query argument using placeholder '?' instead of string concatenation.","secure_code":"import sqlite3\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\nALLOWED_METRICS = {'temperature', 'humidity', 'power_consumption', 'pressure', 'light_level'}\n\n@app.route('/iot/device/telemetry', methods=['GET'])\ndef fetch_device_telemetry():\n    device_mac = request.args.get('mac_address', '')\n    time_range = request.args.get('hours', '24')\n    metric_type = request.args.get('metric', 'temperature')\n\n    # Validate metric_type against allowlist to prevent column injection\n    if metric_type not in ALLOWED_METRICS:\n        return jsonify({'status': 'error', 'message': 'Invalid metric type'}), 400\n\n    # Validate time_range is a positive integer\n    try:\n        time_range_int = int(time_range)\n        if time_range_int <= 0 or time_range_int > 8760:\n            return jsonify({'status': 'error', 'message': 'Invalid time range'}), 400\n    except ValueError:\n        return jsonify({'status': 'error', 'message': 'Time range must be a number'}), 400\n\n    conn = sqlite3.connect('/var/iot/telemetry.db')\n    cursor = conn.cursor()\n    # metric_type is safe (validated against allowlist), time_range_int is safe (validated as int)\n    # device_mac is parameterized to prevent SQL injection\n    query = f\"SELECT timestamp, {metric_type}, device_id FROM sensor_data WHERE mac_address = ? AND timestamp > datetime('now', '-{time_range_int} hours') ORDER BY timestamp DESC\"\n    cursor.execute(query, (device_mac,))\n    results = cursor.fetchall()\n    telemetry_data = []\n    for row in results:\n        telemetry_data.append({\n            'timestamp': row[0],\n            'value': row[1],\n            'device': row[2]\n        })\n    conn.close()\n    return jsonify({'status': 'success', 'data': telemetry_data})"}