{"title":"Expression Injection via pandas.query()","language":"Python","severity":"Critical","cwe":"CWE-95","source_lines":[9],"flow_lines":[9,10],"sink_lines":[10],"vulnerable_code":"import pandas as pd\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\niot_sensor_data = pd.DataFrame({'device_id': [101, 102, 103], 'temperature': [22.5, 24.1, 19.8], 'humidity': [45, 52, 38], 'location': ['warehouse_a', 'warehouse_b', 'warehouse_c']})\n\n@app.route('/api/iot/filter_sensors', methods=['POST'])\ndef filter_sensor_readings():\n    filter_expression = request.json.get('filter_criteria', 'temperature > 20')\n    filtered_results = iot_sensor_data.query(filter_expression)\n    return jsonify(filtered_results.to_dict(orient='records'))\n\nif __name__ == '__main__':\n    app.run(debug=True)","explanation":"The application accepts a user-controlled filter expression through the 'filter_criteria' parameter and passes it directly to pandas DataFrame.query() without validation or sanitization. The query() method evaluates the expression using Python's eval(), allowing arbitrary code execution through expression injection.","remediation":"The fix adds a strict validation layer that whitelists allowed column names, operators, and expression patterns before passing any user input to pandas query(). A regex pattern ensures the expression only contains simple comparisons joined by logical operators, and a blocklist prevents dangerous Python constructs like __import__, eval, exec, and other code injection vectors. Additionally, debug mode is disabled in production to prevent information leakage.","secure_code":"import pandas as pd\nimport re\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\niot_sensor_data = pd.DataFrame({'device_id': [101, 102, 103], 'temperature': [22.5, 24.1, 19.8], 'humidity': [45, 52, 38], 'location': ['warehouse_a', 'warehouse_b', 'warehouse_c']})\n\nALLOWED_COLUMNS = {'device_id', 'temperature', 'humidity', 'location'}\nALLOWED_OPERATORS = {'>', '<', '>=', '<=', '==', '!=', 'and', 'or', 'not'}\nSAFE_EXPRESSION_PATTERN = re.compile(\n    r'^[a-zA-Z_][a-zA-Z0-9_]*\\s*(>|<|>=|<=|==|!=)\\s*([\\d.]+|\"[a-zA-Z0-9_]+\"|\\'[a-zA-Z0-9_]+\\')'\n    r'(\\s+(and|or)\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*(>|<|>=|<=|==|!=)\\s*([\\d.]+|\"[a-zA-Z0-9_]+\"|\\'[a-zA-Z0-9_]+\\'))*$'\n)\n\ndef validate_filter_expression(expression):\n    \"\"\"Validate that the filter expression only contains safe column references and comparison operators.\"\"\"\n    expression = expression.strip()\n\n    if not expression:\n        return False, \"Empty expression\"\n\n    dangerous_patterns = ['__', 'import', 'eval', 'exec', 'compile', 'getattr',\n                          'setattr', 'delattr', 'globals', 'locals', 'vars',\n                          'open', 'file', 'input', 'raw_input', 'reload',\n                          'system', 'popen', 'subprocess', 'os.', 'sys.',\n                          '@', '{', '}', '[', ']', ';', '\\n', '\\r']\n    expression_lower = expression.lower()\n    for pattern in dangerous_patterns:\n        if pattern in expression_lower:\n            return False, f\"Forbidden pattern detected: {pattern}\"\n\n    if not SAFE_EXPRESSION_PATTERN.match(expression):\n        return False, \"Expression does not match allowed format\"\n\n    tokens = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*', expression)\n    for token in tokens:\n        if token not in ALLOWED_COLUMNS and token not in ALLOWED_OPERATORS:\n            return False, f\"Invalid column or keyword: {token}\"\n\n    return True, \"Valid\"\n\n@app.route('/api/iot/filter_sensors', methods=['POST'])\ndef filter_sensor_readings():\n    filter_expression = request.json.get('filter_criteria', 'temperature > 20')\n\n    is_valid, message = validate_filter_expression(filter_expression)\n    if not is_valid:\n        return jsonify({'error': f'Invalid filter expression: {message}'}), 400\n\n    try:\n        filtered_results = iot_sensor_data.query(filter_expression)\n        return jsonify(filtered_results.to_dict(orient='records'))\n    except Exception as e:\n        return jsonify({'error': f'Query execution failed: {str(e)}'}), 400\n\nif __name__ == '__main__':\n    app.run(debug=False)"}