{"title":"Expression Injection via pandas.query()","language":"Python","severity":"Critical","cwe":"CWE-95","source_lines":[1],"flow_lines":[1,2,5],"sink_lines":[5],"vulnerable_code":"def filter_iot_sensor_metrics(device_telemetry_df, threshold_expr):\n    sanitized_expr = threshold_expr.replace(\";\", \"\").strip()\n    if len(sanitized_expr) > 100:\n        return {\"error\": \"Expression too long\"}\n    filtered_data = device_telemetry_df.query(sanitized_expr)\n    aggregated = filtered_data.groupby('device_id').agg({\n        'temperature': 'mean',\n        'humidity': 'mean',\n        'battery_level': 'min'\n    })\n    return aggregated.to_dict('records')","explanation":"The function accepts user-controlled input 'threshold_expr' and performs inadequate sanitization by only removing semicolons. This sanitized but still tainted expression is directly passed to pandas.query(), which evaluates Python expressions, allowing arbitrary code execution through methods like __import__() or attribute access.","remediation":"The fix implements a strict whitelist-based validation approach that only allows known column names, comparison operators, logical keywords, and numeric literals in the expression. It additionally blocks dangerous patterns like double underscores, function calls, and attribute access to prevent code injection through pandas.query().","secure_code":"import re\n\ndef filter_iot_sensor_metrics(device_telemetry_df, threshold_expr):\n    ALLOWED_COLUMNS = {'temperature', 'humidity', 'battery_level', 'device_id', 'timestamp'}\n    ALLOWED_OPERATORS = {'>', '<', '>=', '<=', '==', '!=', 'and', 'or', 'not'}\n    \n    sanitized_expr = threshold_expr.strip()\n    if len(sanitized_expr) > 100:\n        return {\"error\": \"Expression too long\"}\n    \n    # Tokenize and validate the expression against a whitelist\n    tokens = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*|[><=!]+|[0-9]*\\.?[0-9]+', sanitized_expr)\n    \n    for token in tokens:\n        # Check if token is a number\n        if re.fullmatch(r'[0-9]*\\.?[0-9]+', token):\n            continue\n        # Check if token is an allowed column name\n        if token in ALLOWED_COLUMNS:\n            continue\n        # Check if token is an allowed operator keyword\n        if token in ALLOWED_OPERATORS:\n            continue\n        # Check if token is a comparison operator\n        if re.fullmatch(r'[><=!]+', token):\n            if token in {'>', '<', '>=', '<=', '==', '!='}:\n                continue\n        return {\"error\": f\"Disallowed token in expression: '{token}'\"}\n    \n    # Ensure the expression only contains allowed characters\n    if not re.fullmatch(r'[a-zA-Z0-9_.\\s><=!()]+', sanitized_expr):\n        return {\"error\": \"Expression contains disallowed characters\"}\n    \n    # Block dangerous patterns\n    if '__' in sanitized_expr:\n        return {\"error\": \"Expression contains disallowed patterns\"}\n    if re.search(r'[a-zA-Z_][a-zA-Z0-9_]*\\s*\\(', sanitized_expr):\n        return {\"error\": \"Function calls are not allowed in expressions\"}\n    if re.search(r'[a-zA-Z_][a-zA-Z0-9_]*\\.[a-zA-Z_]', sanitized_expr):\n        return {\"error\": \"Attribute access is not allowed in expressions\"}\n    \n    filtered_data = device_telemetry_df.query(sanitized_expr)\n    aggregated = filtered_data.groupby('device_id').agg({\n        'temperature': 'mean',\n        'humidity': 'mean',\n        'battery_level': 'min'\n    })\n    return aggregated.to_dict('records')"}