# Insecure Direct Object Reference via Mass Assignment

Language: Python
Severity: High
CWE: CWE-915

## Source
5

## Flow
5-6-7-8

## Sink
7

## Vulnerable Code
```python
from flask import request, jsonify
from models import db, DeviceProfile
@app.route('/iot/device/<int:dev_id>/config', methods=['PUT'])
def patch_device_cfg(dev_id):
    payload = request.get_json()
    rec = DeviceProfile.query.filter_by(id=dev_id).first_or_404()
    for attr, val in payload.items():
        setattr(rec, attr, val)
    db.session.commit()
    return jsonify({'status': 'updated', 'device': rec.to_dict()}), 200
```

## Explanation

The endpoint accepts arbitrary JSON input via `request.get_json()` and blindly iterates over all keys using `setattr(rec, attr, val)`, allowing any client-supplied field name to overwrite any attribute on the DeviceProfile ORM object. This includes sensitive privileged fields such as `owner_id`, `tenant_id`, `is_admin`, or `firmware_locked` that should never be user-controllable. There is no allowlist or field validation to restrict which attributes can be modified.

## Remediation

The fix introduces a strict allowlist (`ALLOWED_CONFIG_FIELDS`) of fields that clients are permitted to modify. Before any attribute is written, the handler validates the incoming JSON payload and rejects requests containing keys outside the allowlist with a 403 response. The iteration then only processes keys that exist in both the allowlist and the payload, preventing attackers from overwriting privileged fields like `owner_id`, `is_admin`, `tenant_id`, or `firmware_locked`.

## Secure Code
```python
from flask import request, jsonify
from models import db, DeviceProfile

ALLOWED_CONFIG_FIELDS = frozenset([
    'device_name',
    'firmware_version',
    'sampling_interval',
    'reporting_endpoint',
    'power_mode',
    'wifi_ssid',
    'wifi_password',
    'timezone',
    'led_enabled',
])

@app.route('/iot/device/<int:dev_id>/config', methods=['PUT'])
def patch_device_cfg(dev_id):
    payload = request.get_json()
    if not payload or not isinstance(payload, dict):
        return jsonify({'error': 'Invalid or missing JSON body'}), 400

    # Reject any keys that are not in the explicit allowlist
    disallowed_keys = set(payload.keys()) - ALLOWED_CONFIG_FIELDS
    if disallowed_keys:
        return jsonify({
            'error': 'Modification of the following fields is not permitted',
            'fields': sorted(disallowed_keys)
        }), 403

    rec = DeviceProfile.query.filter_by(id=dev_id).first_or_404()

    for attr in ALLOWED_CONFIG_FIELDS:
        if attr in payload:
            setattr(rec, attr, payload[attr])

    db.session.commit()
    return jsonify({'status': 'updated', 'device': rec.to_dict()}), 200
```
