# Insecure Deserialization via shelve Module with Untrusted Pickle-Backed Storage

Language: Python
Severity: Critical
CWE: CWE-94

## Source
11

## Flow
11-12

## Sink
12

## Vulnerable Code
```python
import shelve
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
SESSION_DB = '/tmp/iot_sessions'
@app.route('/api/device/restore', methods=['POST'])
def restore_device_config():
    dev_token = request.headers.get('X-Device-Token')
    payload_key = request.json.get('config_key')
    with shelve.open(SESSION_DB) as cfg_store:
        raw_cfg = request.json.get('restore_blob')
        cfg_store[payload_key] = eval(raw_cfg)
        device_cfg = cfg_store[payload_key]
    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
```python
import json
import os
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
CONFIG_DB_PATH = '/tmp/iot_sessions.json'
ALLOWED_CONFIG_KEYS = frozenset(['network', 'display', 'sensors', 'firmware', 'logging'])

def _load_config_db():
    """Load the JSON-backed config store."""
    if os.path.exists(CONFIG_DB_PATH):
        with open(CONFIG_DB_PATH, 'r') as f:
            return json.load(f)
    return {}

def _save_config_db(data):
    """Save the config store as JSON."""
    with open(CONFIG_DB_PATH, 'w') as f:
        json.dump(data, f)

def _validate_config_value(value):
    """Recursively validate that a config value contains only safe JSON-compatible types."""
    if isinstance(value, (str, int, float, bool, type(None))):
        return True
    if isinstance(value, list):
        return all(_validate_config_value(item) for item in value)
    if isinstance(value, dict):
        return all(
            isinstance(k, str) and _validate_config_value(v)
            for k, v in value.items()
        )
    return False

@app.route('/api/device/restore', methods=['POST'])
def restore_device_config():
    dev_token = request.headers.get('X-Device-Token')
    if not dev_token:
        return jsonify({'status': 'error', 'message': 'Missing device token'}), 401

    request_data = request.get_json(silent=True)
    if not request_data:
        return jsonify({'status': 'error', 'message': 'Invalid JSON body'}), 400

    payload_key = request_data.get('config_key')
    if not isinstance(payload_key, str) or payload_key not in ALLOWED_CONFIG_KEYS:
        return jsonify({'status': 'error', 'message': 'Invalid or disallowed config_key'}), 400

    restore_blob = request_data.get('restore_blob')
    if restore_blob is None:
        return jsonify({'status': 'error', 'message': 'Missing restore_blob'}), 400

    if isinstance(restore_blob, str):
        try:
            parsed_config = json.loads(restore_blob)
        except (json.JSONDecodeError, ValueError):
            return jsonify({'status': 'error', 'message': 'restore_blob must be valid JSON'}), 400
    elif isinstance(restore_blob, (dict, list, int, float, bool, type(None))):
        parsed_config = restore_blob
    else:
        return jsonify({'status': 'error', 'message': 'Invalid restore_blob type'}), 400

    if not _validate_config_value(parsed_config):
        return jsonify({'status': 'error', 'message': 'Config contains disallowed data types'}), 400

    cfg_store = _load_config_db()
    cfg_store[payload_key] = parsed_config
    _save_config_db(cfg_store)

    device_cfg = cfg_store[payload_key]
    return jsonify({'status': 'restored', 'config': device_cfg})
```
