VNX-639: Authorization Bypass Through User-Controlled Key
Detects insecure direct object references: an ORM or repository lookup by an id taken from the request (req.params, request.GET, Rails params, @PathVariable) with no owner or authorization check nearby, and owner keys such as UserId filled from the request body. Java, JavaScript/TypeScript, Python and Ruby.
| Field | Value |
|---|---|
| Rule ID | VNX-639 |
| Severity | High |
| SARIF level | error |
| Kind | sast |
| Languages | Java, JavaScript / TypeScript, Python, Ruby |
| CWE | CWE-639 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:H/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:H/DI:H/EX:H/EC:N/P:H |
| Tags | idor authorization cwe-639 |
| Source | internal/sast/rules/vnx-639.rego |
Overview
VNX-639 maps to CWE-639: Authorization Bypass Through User-Controlled Key. A handler that loads /orders/42 by the id in the URL, and never asks whether order 42 belongs to the caller, lets any logged-in user read or change every other user’s records by counting.
Reported:
- A lookup by a client-supplied id. Express/Mongoose/Sequelize/Prisma
findById,findByPk,findOne,findUnique,findFirst,findByIdAndUpdate/Delete,destroy,deleteOne,updateOne, Prisma/Sequelizeupdate/delete/upsertand DAOget...By...Idmethods; Djangoobjects.get/filter/excludeandget_object_or_404, Flask-SQLAlchemyquery.get/get_or_404/filter_by; Spring Data and DAOfindById,getById,getOne,getReferenceById,deleteById,findBy...IdandEntityManager.find; RailsModel.find,find_by,find_by_id,destroy,delete. The id comes fromreq.params/query/body,request.GET/POST/args/form/values/json/data/query_params/COOKIES, Railsparamsorrequest.getParameter, inline, through a variable assigned or destructured from it in the 12 lines above, or through a Spring@PathVariable/@RequestParamparameter. It is reported only when no owner or authorization check appears in the call or within 10 lines before and 6 after. - An owner key from the client. A JavaScript lookup (including findMany, findAll, count, deleteMany and updateMany, with the field on the next line of a multi-line call) whose
UserId,userId,user_id,ownerIdoraccountIdfield is filled straight fromreq.body,req.paramsorreq.query: the key that should come from the session is chosen by the caller.
What Gets Flagged
const order = await Order.findById(req.params.id); // FLAGGED
const wallet = await WalletModel.findOne({ where: { UserId: req.query.UserId } }); // FLAGGED
inv = Invoice.objects.get(id=request.GET["invoice_id"]) # FLAGGED
public Account get(@PathVariable("accountId") Long accountId) {
return accountRepository.findById(accountId).orElseThrow(); // FLAGGED
}
@user = User.find_by(id: params[:user_id]) # FLAGGED
What Is Not Flagged
- Lookups scoped to the caller:
Order.findOne({ _id: id, owner: req.user.id }),objects.get(..., user=request.user),current_user.orders.find(params[:id]). - A nearby ownership or policy check:
if (doc.ownerId !== req.user.id) return res.status(403),@PreAuthorize,authorize!,PermissionDenied. - Lookups by non-identifier fields (
email), session ids, tokens and captcha ids, and keys with no visible request origin. - Test code, comments and string literals.
Authorization enforced in middleware, a policy class or another function is not visible line by line and still produces a finding; suppress it with a comment naming the control.
Remediation
Derive the owner from the authenticated session and put it in the query:
const order = await Order.findOne({ _id: req.params.id, owner: req.user.id });
if (!order) return res.status(404).end();
References
- CWE-639: Authorization Bypass Through User-Controlled 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-639
// nosec vnx-639
Or record an ignore rule: vulnetix ignore add --rule vnx-639 --reason "...". See Suppressing findings.