# Insecure Direct Object Reference (IDOR)

Language: Python
Severity: Critical
CWE: CWE-639

## Source
9

## Flow
9-11

## Sink
11

## Vulnerable Code
```python
from flask import Flask, request, jsonify
from pymongo import MongoClient
from bson import ObjectId
app = Flask(__name__)
db = MongoClient('mongodb://localhost:27017/')['iot_platform']
@app.route('/api/device/telemetry', methods=['GET'])
def fetch_device_telemetry():
    dev_id = request.args.get('device_id')
    rec = db.telemetry.find_one({'_id': ObjectId(dev_id)})
    if not rec:
        return jsonify({'err': 'device not found'}), 404
    return jsonify({'owner': str(rec['owner_id']), 'readings': rec['sensor_data'], 'secret_key': rec['api_secret']}), 200
```

## Explanation

The endpoint accepts a user-supplied 'device_id' query parameter and directly uses it to query MongoDB without any authorization check verifying that the requesting user owns or has permission to access the specified device. Any authenticated (or unauthenticated) user can enumerate valid MongoDB ObjectIds and retrieve any device's telemetry, owner information, and API secret keys belonging to other users.

## Remediation

The fix adds three layers of protection: (1) an authentication decorator (require_auth) ensures only authenticated users can reach the endpoint and populates g.current_user_id; (2) the MongoDB query is scoped by both _id and owner_id, so a user can only retrieve telemetry for devices they own, eliminating the IDOR; (3) the sensitive api_secret field is no longer included in the response, and the ObjectId conversion is wrapped in a try/except to handle malformed input gracefully.

## Secure Code
```python
from flask import Flask, request, jsonify, g
from pymongo import MongoClient
from bson import ObjectId
from bson.errors import InvalidId
from functools import wraps

app = Flask(__name__)
db = MongoClient('mongodb://localhost:27017/')['iot_platform']


def require_auth(f):
    """Decorator that authenticates the request and populates g.current_user_id."""
    @wraps(f)
    def decorated(*args, **kwargs):
        # --- Replace the following stub with your real authentication logic ---
        # e.g. validate a JWT / session token from the Authorization header
        auth_header = request.headers.get('Authorization', '')
        if not auth_header:
            return jsonify({'err': 'authentication required'}), 401

        user_id = _verify_token(auth_header)  # must return a valid user id or None
        if user_id is None:
            return jsonify({'err': 'invalid or expired token'}), 401

        g.current_user_id = user_id
        return f(*args, **kwargs)
    return decorated


def _verify_token(auth_header: str):
    """Stub – implement real token verification (JWT, OAuth, etc.)."""
    # Return the authenticated user's ID (as a string) or None on failure.
    # Example: return decode_jwt(auth_header.split(' ')[1])['sub']
    return None


@app.route('/api/device/telemetry', methods=['GET'])
@require_auth
def fetch_device_telemetry():
    dev_id = request.args.get('device_id')
    if not dev_id:
        return jsonify({'err': 'device_id is required'}), 400

    # Safely convert the supplied id; reject malformed values.
    try:
        device_oid = ObjectId(dev_id)
    except (InvalidId, TypeError):
        return jsonify({'err': 'invalid device_id format'}), 400

    # Query scoped to the authenticated user's ownership — prevents IDOR.
    rec = db.telemetry.find_one({
        '_id': device_oid,
        'owner_id': g.current_user_id
    })

    if not rec:
        # Intentionally vague to avoid leaking whether the id exists for another owner.
        return jsonify({'err': 'device not found'}), 404

    # Do NOT expose sensitive credentials such as api_secret to the client.
    return jsonify({
        'owner': str(rec['owner_id']),
        'readings': rec['sensor_data']
    }), 200
```
