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).
Summary
update_user_policybuilds a SQLUPDATEby string-concatenating JSON keys directly into theSETclause 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-336What goes wrong
The handler iterates the raw request JSON and interpolates each key into an f-string:
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 theSETclause. Optionally build the query with SQLAlchemy Core / a query builder so identifiers are quoted safely.Severity
Critical (security).