VNX-566: Authorization Bypass Through User-Controlled SQL Primary Key
Reports a Java or Python SELECT, UPDATE or DELETE whose only WHERE condition is a key column compared with a request-supplied value, with no ownership condition in the query and no ownership check nearby.
| Field | Value |
|---|---|
| Rule ID | VNX-566 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Java, Python |
| CWE | CWE-566 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | sql-auth-bypass idor cwe-566 |
| Source | internal/sast/rules/vnx-566.rego |
Overview
VNX-566 maps to CWE-566: Authorization Bypass Through User-Controlled SQL Primary Key. It reports a query whose only WHERE condition is a key column (id, uuid, <name>_id, <name>Id) compared with a value the request supplied. Nothing in the query ties the row to the caller, so changing the key in the URL or form reads or changes somebody else’s record (an insecure direct object reference).
What Gets Flagged
The key counts as request-supplied when it comes from request.getParameter/getHeader, a @PathVariable/@RequestParam/@PathParam/@QueryParam parameter, Flask request.args/form/values/json, Django request.GET/POST or a Django view argument, or a Flask route placeholder, and it reaches the statement: concatenated, interpolated, passed as a bind argument, or bound with ps.setX(n, key) on the next lines.
String invoiceId = request.getParameter("invoiceId");
PreparedStatement ps = c.prepareStatement("SELECT amount, customer FROM invoices WHERE id = ?");
ps.setString(1, invoiceId);
@app.route("/profile/<int:profile_id>")
def show_profile(profile_id):
row = query(f"SELECT name, email, phone FROM profiles WHERE profile_id = {profile_id}")
def document_detail(request, doc_id):
with connection.cursor() as cursor:
cursor.execute("SELECT title, body FROM documents WHERE id = %s", [doc_id])
What Is Not Flagged
- A query with a further condition:
WHERE id = ? AND customer_id = ?, also when theANDis on the next concatenated line. - A key from the session or another server-side value, and constant keys (
WHERE id = 1). - A nearby ownership or authorization check:
owner,current_user,request.user,getUserPrincipal,@PreAuthorize,@Secured,@RolesAllowed,permission_required. - A key already passed to a lookup function a few lines above (
get_post(id)), which commonly loads the record and checks its owner. - SQL in comments and in strings that are not queries.
Ownership enforced in another function the rule cannot see, a database row-level security policy and ORM lookups are out of scope.
Remediation
- Scope the query to the caller:
SELECT ... FROM invoices WHERE id = ? AND customer_id = ?, binding the customer from the authenticated session, not from the request. - Or load the row and compare its owner with the authenticated user before returning or changing it, answering 404 when they differ.
- Prefer unguessable identifiers (UUIDs) as defence in depth; they do not replace the check.
References
- CWE-566: Authorization Bypass Through User-Controlled SQL Primary Key
- OWASP: Insecure Direct Object Reference Prevention Cheat Sheet
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-566
// nosec vnx-566
Or record an ignore rule: vulnetix ignore add --rule vnx-566 --reason "...". See Suppressing findings.