{"title":"Insecure Deserialization via shelve Module with Untrusted Pickle-Backed Storage","language":"Python","severity":"Critical","cwe":"CWE-94","source_lines":[11],"flow_lines":[11,12],"sink_lines":[12],"vulnerable_code":"import shelve\nimport os\nfrom flask import Flask, request, jsonify\napp = Flask(__name__)\nSESSION_DB = '/tmp/iot_sessions'\n@app.route('/api/device/restore', methods=['POST'])\ndef restore_device_config():\n    dev_token = request.headers.get('X-Device-Token')\n    payload_key = request.json.get('config_key')\n    with shelve.open(SESSION_DB) as cfg_store:\n        raw_cfg = request.json.get('restore_blob')\n        cfg_store[payload_key] = eval(raw_cfg)\n        device_cfg = cfg_store[payload_key]\n    return jsonify({'status': 'restored', 'config': str(device_cfg)})","explanation":"The `restore_blob` value is taken directly from untrusted user-controlled JSON input (line 11) and passed without any sanitization to `eval()` (line 12). This allows an attacker to inject arbitrary Python expressions that will be executed on the server with the full privileges of the running process. Additionally, the result is stored via `shelve` (which uses `pickle` internally), creating a secondary deserialization risk on retrieval.","remediation":"The fix eliminates both vulnerabilities: (1) the dangerous `eval()` call is replaced with `json.loads()` for safe parsing of the restore_blob, with strict validation that only JSON-safe types are accepted; (2) the `shelve` module (which uses `pickle` internally) is replaced with a plain JSON file-backed store, completely removing the insecure deserialization surface. Additionally, input validation is added for `config_key` using an allowlist, the device token is checked for presence, and all inputs are type-checked before processing.","secure_code":"import json\nimport os\nimport hmac\nimport hashlib\nfrom flask import Flask, request, jsonify\n\napp = Flask(__name__)\nCONFIG_DB_PATH = '/tmp/iot_sessions.json'\nALLOWED_CONFIG_KEYS = frozenset(['network', 'display', 'sensors', 'firmware', 'logging'])\n\ndef _load_config_db():\n    \"\"\"Load the JSON-backed config store.\"\"\"\n    if os.path.exists(CONFIG_DB_PATH):\n        with open(CONFIG_DB_PATH, 'r') as f:\n            return json.load(f)\n    return {}\n\ndef _save_config_db(data):\n    \"\"\"Save the config store as JSON.\"\"\"\n    with open(CONFIG_DB_PATH, 'w') as f:\n        json.dump(data, f)\n\ndef _validate_config_value(value):\n    \"\"\"Recursively validate that a config value contains only safe JSON-compatible types.\"\"\"\n    if isinstance(value, (str, int, float, bool, type(None))):\n        return True\n    if isinstance(value, list):\n        return all(_validate_config_value(item) for item in value)\n    if isinstance(value, dict):\n        return all(\n            isinstance(k, str) and _validate_config_value(v)\n            for k, v in value.items()\n        )\n    return False\n\n@app.route('/api/device/restore', methods=['POST'])\ndef restore_device_config():\n    dev_token = request.headers.get('X-Device-Token')\n    if not dev_token:\n        return jsonify({'status': 'error', 'message': 'Missing device token'}), 401\n\n    request_data = request.get_json(silent=True)\n    if not request_data:\n        return jsonify({'status': 'error', 'message': 'Invalid JSON body'}), 400\n\n    payload_key = request_data.get('config_key')\n    if not isinstance(payload_key, str) or payload_key not in ALLOWED_CONFIG_KEYS:\n        return jsonify({'status': 'error', 'message': 'Invalid or disallowed config_key'}), 400\n\n    restore_blob = request_data.get('restore_blob')\n    if restore_blob is None:\n        return jsonify({'status': 'error', 'message': 'Missing restore_blob'}), 400\n\n    if isinstance(restore_blob, str):\n        try:\n            parsed_config = json.loads(restore_blob)\n        except (json.JSONDecodeError, ValueError):\n            return jsonify({'status': 'error', 'message': 'restore_blob must be valid JSON'}), 400\n    elif isinstance(restore_blob, (dict, list, int, float, bool, type(None))):\n        parsed_config = restore_blob\n    else:\n        return jsonify({'status': 'error', 'message': 'Invalid restore_blob type'}), 400\n\n    if not _validate_config_value(parsed_config):\n        return jsonify({'status': 'error', 'message': 'Config contains disallowed data types'}), 400\n\n    cfg_store = _load_config_db()\n    cfg_store[payload_key] = parsed_config\n    _save_config_db(cfg_store)\n\n    device_cfg = cfg_store[payload_key]\n    return jsonify({'status': 'restored', 'config': device_cfg})"}