# Expression Injection via pandas.query()

Language: Python
Severity: Critical
CWE: CWE-95

## Source
1

## Flow
1-2-5

## Sink
5

## Vulnerable Code
```python
def filter_iot_sensor_metrics(device_telemetry_df, threshold_expr):
    sanitized_expr = threshold_expr.replace(";", "").strip()
    if len(sanitized_expr) > 100:
        return {"error": "Expression too long"}
    filtered_data = device_telemetry_df.query(sanitized_expr)
    aggregated = filtered_data.groupby('device_id').agg({
        'temperature': 'mean',
        'humidity': 'mean',
        'battery_level': 'min'
    })
    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
```python
import re

def filter_iot_sensor_metrics(device_telemetry_df, threshold_expr):
    ALLOWED_COLUMNS = {'temperature', 'humidity', 'battery_level', 'device_id', 'timestamp'}
    ALLOWED_OPERATORS = {'>', '<', '>=', '<=', '==', '!=', 'and', 'or', 'not'}
    
    sanitized_expr = threshold_expr.strip()
    if len(sanitized_expr) > 100:
        return {"error": "Expression too long"}
    
    # Tokenize and validate the expression against a whitelist
    tokens = re.findall(r'[a-zA-Z_][a-zA-Z0-9_]*|[><=!]+|[0-9]*\.?[0-9]+', sanitized_expr)
    
    for token in tokens:
        # Check if token is a number
        if re.fullmatch(r'[0-9]*\.?[0-9]+', token):
            continue
        # Check if token is an allowed column name
        if token in ALLOWED_COLUMNS:
            continue
        # Check if token is an allowed operator keyword
        if token in ALLOWED_OPERATORS:
            continue
        # Check if token is a comparison operator
        if re.fullmatch(r'[><=!]+', token):
            if token in {'>', '<', '>=', '<=', '==', '!='}:
                continue
        return {"error": f"Disallowed token in expression: '{token}'"}
    
    # Ensure the expression only contains allowed characters
    if not re.fullmatch(r'[a-zA-Z0-9_.\s><=!()]+', sanitized_expr):
        return {"error": "Expression contains disallowed characters"}
    
    # Block dangerous patterns
    if '__' in sanitized_expr:
        return {"error": "Expression contains disallowed patterns"}
    if re.search(r'[a-zA-Z_][a-zA-Z0-9_]*\s*\(', sanitized_expr):
        return {"error": "Function calls are not allowed in expressions"}
    if re.search(r'[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_]', sanitized_expr):
        return {"error": "Attribute access is not allowed in expressions"}
    
    filtered_data = device_telemetry_df.query(sanitized_expr)
    aggregated = filtered_data.groupby('device_id').agg({
        'temperature': 'mean',
        'humidity': 'mean',
        'battery_level': 'min'
    })
    return aggregated.to_dict('records')
```
