HIGH insecure direct object referenceflaskapi keys

Insecure Direct Object Reference in Flask with Api Keys

Insecure Direct Object Reference in Flask with Api Keys — how this specific combination creates or exposes the vulnerability

Insecure Direct Object Reference (BOLA/IDOR) in Flask occurs when an API endpoint uses user-supplied identifiers to access resources without verifying that the requesting identity is authorized for that specific object. Combining this with API keys as the sole authorization mechanism can expose sensitive records because the key identifies the client application or user, but the endpoint may still allow an attacker to iterate or manipulate object IDs (e.g., /users/123, /invoices/456) without enforcing ownership or scope checks.

Consider a Flask endpoint that retrieves a user profile by ID and expects an API key in the Authorization header:

from flask import Flask, request, jsonify

app = Flask(__name__)

# Example datastore
users = {
    1: {"id": 1, "name": "alice", "email": "alice@example.com", "api_key": "alice_key"},
    2: {"id": 2, "name": "bob", "email": "bob@example.com", "api_key": "bob_key"},
}

@app.route("/users/", methods=["GET"])
def get_user(user_id):
    api_key = request.headers.get("X-API-Key")
    if not api_key:
        return jsonify({"error": "API key missing"}), 401
    # Vulnerable: key only checked for existence, not bound to user_id
    user = users.get(user_id)
    if not user:
        return jsonify({"error": "not found"}), 404
    if user["api_key"] != api_key:
        return jsonify({"error": "forbidden"}), 403
    return jsonify({"id": user["id"], "name": user["name"], "email": user["email"]})

In this example, the API key is compared to a per-user key, but note that the check occurs after the object is retrieved by ID. An authenticated client with a valid key for user 1 could attempt to access /users/2 by changing the URL parameter. If the application does not validate that the key matches the requested user_id, the response may reveal existence or details of user 2, resulting in an IDOR. Even when the key matches, missing ownership validation at the route level means horizontal privilege escalation is possible if the key is leaked or reused across accounts.

Another common pattern is using API keys to identify an integration or service account that should only access a subset of resources. If the key is mapped to a broad role or the endpoint does not scope queries by the key’s associated tenant or group, an attacker who knows or guesses another valid ID may read or alter data belonging to others. For example:

@app.route("/invoices/", methods=["GET"])
def get_invoice(invoice_id):
    api_key = request.headers.get("X-API-Key")
    if not api_key:
        return jsonify({"error": "API key missing"}), 401
    # Vulnerable: no check that invoice belongs to the key’s tenant
    invoice = get_invoice_from_db(invoice_id)  # hypothetical DB call
    if not invoice:
        return jsonify({"error": "not found"}), 404
    return jsonify(invoice)

Here, the API key might identify a merchant or customer tenant, but if the SQL query or ORM call does not filter by tenant_id, an attacker can enumerate invoice IDs and access data outside their tenant. This becomes a vertical IDOR if the key maps to a lower-privileged role that should not access certain invoice types. The risk is compounded when error messages differ between not found and unauthorized, allowing attackers to infer existence of resources.

middleBrick detects such patterns during scans by correlating authentication (API key usage) with authorization checks across endpoints and, where applicable, cross-referencing OpenAPI/Swagger definitions with runtime behavior to highlight missing ownership or scope constraints.

Api Keys-Specific Remediation in Flask — concrete code fixes

To remediate IDOR when using API keys in Flask, enforce strict ownership and scope checks before returning any object. Instead of treating the API key as a simple boolean credential, bind it to the requested resource and ensure the data access layer applies filters consistently.

1) Key-to-owner binding with parameterized queries:

from flask import Flask, request, jsonify

app = Flask(__name__)

# Example datastore
users = {
    1: {"id": 1, "name": "alice", "email": "alice@example.com", "api_key": "alice_key"},
    2: {"id": 2, "name": "bob", "email": "bob@example.com", "api_key": "bob_key"},
}

@app.route("/users/", methods=["GET"])
def get_user(user_id):
    api_key = request.headers.get("X-API-Key")
    if not api_key:
        return jsonify({"error": "API key missing"}), 401
    # Correct: find by key first, then verify requested ID matches
    owner = None
    for uid, u in users.items():
        if u["api_key"] == api_key:
            owner = u
            break
    if owner is None:
        return jsonify({"error": "forbidden"}), 403
    if owner["id"] != user_id:
        return jsonify({"error": "forbidden"}), 403
    return jsonify({"id": owner["id"], "name": owner["name"], "email": owner["email"]})

This ensures the key maps to a specific user and that the requested user_id equals the key’s owner, preventing horizontal IDOR.

2) Scoped data access for tenant-aware keys:

@app.route("/invoices/", methods=["GET"])
def get_invoice(invoice_id):
    api_key = request.headers.get("X-API-Key")
    if not api_key:
        return jsonify({"error": "API key missing"}), 401
    # Assume get_tenant_by_api_key returns tenant metadata or None
    tenant = get_tenant_by_api_key(api_key)
    if not tenant:
        return jsonify({"error": "forbidden"}), 403
    # Enforce tenant scope in the data access layer
    invoice = get_invoice_from_db(invoice_id, tenant_id=tenant["id"])
    if not invoice:
        return jsonify({"error": "not found"}), 404
    return jsonify(invoice)

def get_invoice_from_db(invoice_id, tenant_id):
    # Example SQL-like filter; implement with parameterized queries
    # SELECT * FROM invoices WHERE id = ? AND tenant_id = ?
    pass

By filtering at the query level using the tenant derived from the API key, you ensure that even if an attacker guesses another invoice ID, the database returns no rows because the tenant_id does not match. This approach aligns with the principle of least privilege and prevents both horizontal and vertical IDOR.

3) Use a mapping table or claims in the key itself to avoid iterating over users:

import hashlib
import hmac

# Example: derive a key binding to user_id without storing plaintext mapping
SECRET = b"rotate_me_securely"
def derive_key(user_id: int) -> str:
    return hmac.new(SECRET, str(user_id).encode(), hashlib.sha256).hexdigest()

def verify_key(user_id: int, provided: str) -> bool:
    return hmac.compare_digest(derive_key(user_id), provided)

@app.route("/users/", methods=["GET"])
def get_user(user_id):
    api_key = request.headers.get("X-API-Key")
    if not api_key or not verify_key(user_id, api_key):
        return jsonify({"error": "forbidden"}), 403
    return jsonify({"id": user_id, "name": "alice", "email": "alice@example.com"})

This method binds the key to the ID cryptographically, so the server does not need to look up a per-user key, and tampering with the ID results in verification failure. Ensure keys are transmitted over TLS and rotated periodically.

middleBrick’s scans can validate these fixes by checking that endpoints which use API keys also enforce object-level authorization and that error handling does not leak information that could aid enumeration.

Related CWEs: bolaAuthorization

CWE IDNameSeverity
CWE-250Execution with Unnecessary Privileges HIGH
CWE-639Insecure Direct Object Reference CRITICAL
CWE-732Incorrect Permission Assignment HIGH

Frequently Asked Questions

Can API keys alone prevent IDOR if they are kept secret?
No. API keys identify the client or integration but do not imply object-level permissions. Without explicit ownership or scope checks, an authenticated client can still reference other valid IDs and access or manipulate resources they should not see or change.
How can I test my Flask endpoints for IDOR without a scanner?
You can manually test by using two distinct API keys (representing different users or tenants) and attempting to access each key’s resources using the other key’s object IDs. Ensure that 403 responses are returned for cross-ownership requests and that error messages do not reveal whether a resource exists.