Skip to content

SQL injection in update_user_policy via unparameterized dict keys #3445

Description

@MaxGhenis

Summary

update_user_policy builds a SQL UPDATE by string-concatenating JSON keys directly into the SET clause with no allow-list. Any attacker with access to this route can inject arbitrary SQL by supplying a crafted key name.

Location

policyengine_api/endpoints/policy.py:330-336

What goes wrong

The handler iterates the raw request JSON and interpolates each key into an f-string:

setter_array = []
args = []
payload = request.json
user_policy_id = payload.pop("id")

for key in payload:
    setter_array.append(f"{key} = ?")
    args.append(payload[key])
setter_phrase = ", ".join(setter_array)

args.append(user_policy_id)
sql_request = f"UPDATE user_policies SET {setter_phrase} WHERE id = ?"

Only the values are parameterized; the keys are literal text. A payload like

{
  "id": 1,
  "username, password = (select password from admins limit 1) -- ": "x"
}

results in raw attacker-controlled SQL being sent to the database. This works even without a privileged account because the endpoint is otherwise reachable (see #3395 for the related auth gap on saved-policy routes).

Suggested fix

Whitelist the allowed column names (e.g., ALLOWED = {"label", "notes", ...}) and reject or drop any key not in the set before building the SET clause. Optionally build the query with SQLAlchemy Core / a query builder so identifiers are quoted safely.

Severity

Critical (security).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity issues

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions