{"title":"Insecure Deserialization via pickle.loads()","language":"Python","severity":"Critical","cwe":"CWE-502","source_lines":[6],"flow_lines":[6,8,9],"sink_lines":[9],"vulnerable_code":"import pickle\nimport redis\nfrom multiprocessing import Pool\ndef fetch_and_execute_sensor_task(device_id):\n    r = redis.StrictRedis(host='iot-broker', port=6379, db=0)\n    raw_payload = r.lpop(f'sensor_queue:{device_id}')\n    if not raw_payload:\n        return None\n    task_obj = pickle.loads(raw_payload)\n    result = task_obj['handler'](task_obj['params'])\n    return result\ndef dispatch_sensor_workers(dev_ids):\n    with Pool(processes=4) as wp:\n        outcomes = wp.map(fetch_and_execute_sensor_task, dev_ids)\n    return outcomes","explanation":"The raw_payload retrieved from Redis at line 6 is untrusted external data that flows directly into pickle.loads() at line 9 without any validation, signature verification, or integrity check. An attacker who can write to the Redis queue (e.g., via a compromised IoT device, network interception, or direct Redis access) can inject a crafted pickle payload that executes arbitrary OS commands during deserialization, before any application logic runs.","remediation":"The fix eliminates pickle.loads() entirely, replacing it with JSON deserialization which cannot execute arbitrary code during parsing. An HMAC-SHA256 signature is required on every payload to ensure integrity and authenticity before any deserialization occurs, preventing tampered or injected messages from being processed. Additionally, a handler whitelist ensures only explicitly registered function names can be invoked, blocking any attempt to call arbitrary callables even if the JSON content were somehow manipulated.","secure_code":"import json\nimport hmac\nimport hashlib\nimport redis\nfrom multiprocessing import Pool\n\n# HMAC secret key — must be stored securely (e.g., env var, secrets manager)\n# and shared only between trusted producer and consumer components.\nimport os\nHMAC_SECRET_KEY = os.environ.get('SENSOR_QUEUE_HMAC_KEY', '').encode('utf-8')\n\n# Whitelist of allowed handler function names mapped to actual callables.\n# Only explicitly registered handlers can be invoked by task payloads.\nALLOWED_HANDLERS = {}\n\ndef register_handler(name, func):\n    \"\"\"Register a callable handler by name for task dispatch.\"\"\"\n    if not callable(func):\n        raise ValueError(f\"Handler '{name}' must be callable\")\n    ALLOWED_HANDLERS[name] = func\n\ndef _verify_hmac(message_bytes, received_mac_hex):\n    \"\"\"Verify HMAC-SHA256 signature of the message.\"\"\"\n    if not HMAC_SECRET_KEY:\n        raise RuntimeError(\"SENSOR_QUEUE_HMAC_KEY environment variable is not set\")\n    expected_mac = hmac.new(HMAC_SECRET_KEY, message_bytes, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected_mac, received_mac_hex)\n\ndef _validate_task(task_obj):\n    \"\"\"Validate the structure and contents of a deserialized task object.\"\"\"\n    if not isinstance(task_obj, dict):\n        raise ValueError(\"Task object must be a dictionary\")\n    if 'handler' not in task_obj or 'params' not in task_obj:\n        raise ValueError(\"Task object must contain 'handler' and 'params' keys\")\n    handler_name = task_obj['handler']\n    if not isinstance(handler_name, str):\n        raise ValueError(\"Handler must be a string name\")\n    if handler_name not in ALLOWED_HANDLERS:\n        raise ValueError(f\"Unknown or disallowed handler: '{handler_name}'\")\n    if not isinstance(task_obj['params'], (dict, list, str, int, float, bool, type(None))):\n        raise ValueError(\"Params must be a JSON-safe primitive or container type\")\n    return handler_name, task_obj['params']\n\ndef fetch_and_execute_sensor_task(device_id):\n    r = redis.StrictRedis(host='iot-broker', port=6379, db=0)\n    raw_payload = r.lpop(f'sensor_queue:{device_id}')\n    if not raw_payload:\n        return None\n\n    # Expect payload format: \"<hex_hmac>:<json_body>\"\n    try:\n        raw_str = raw_payload.decode('utf-8')\n    except (UnicodeDecodeError, AttributeError):\n        raise ValueError(\"Payload is not valid UTF-8\")\n\n    if ':' not in raw_str:\n        raise ValueError(\"Payload missing HMAC signature prefix\")\n\n    received_mac, json_body = raw_str.split(':', 1)\n\n    # Step 1: Verify HMAC integrity/authenticity before parsing\n    if not _verify_hmac(json_body.encode('utf-8'), received_mac):\n        raise ValueError(\"HMAC verification failed — payload integrity compromised\")\n\n    # Step 2: Deserialize using safe JSON (no arbitrary code execution)\n    task_obj = json.loads(json_body)\n\n    # Step 3: Validate structure and whitelist the handler\n    handler_name, params = _validate_task(task_obj)\n\n    # Step 4: Invoke only the whitelisted handler\n    result = ALLOWED_HANDLERS[handler_name](params)\n    return result\n\ndef dispatch_sensor_workers(dev_ids):\n    with Pool(processes=4) as wp:\n        outcomes = wp.map(fetch_and_execute_sensor_task, dev_ids)\n    return outcomes\n\n# --- Example: Producer-side helper (for trusted task publishers) ---\n# def publish_task(device_id, handler_name, params):\n#     import json, hmac, hashlib, os, redis\n#     secret = os.environ['SENSOR_QUEUE_HMAC_KEY'].encode('utf-8')\n#     body = json.dumps({'handler': handler_name, 'params': params})\n#     mac = hmac.new(secret, body.encode('utf-8'), hashlib.sha256).hexdigest()\n#     r = redis.StrictRedis(host='iot-broker', port=6379, db=0)\n#     r.lpush(f'sensor_queue:{device_id}', f'{mac}:{body}')"}