Skip to content

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.

Updated 02 Nov 2026Checked against cli@de9b52a
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/Sequelize update/delete/upsert and DAO get...By...Id methods; Django objects.get/filter/exclude and get_object_or_404, Flask-SQLAlchemy query.get/get_or_404/filter_by; Spring Data and DAO findById, getById, getOne, getReferenceById, deleteById, findBy...Id and EntityManager.find; Rails Model.find, find_by, find_by_id, destroy, delete. The id comes from req.params/query/body, request.GET/POST/args/form/values/json/data/query_params/COOKIES, Rails params or request.getParameter, inline, through a variable assigned or destructured from it in the 12 lines above, or through a Spring @PathVariable/@RequestParam parameter. 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, ownerId or accountId field is filled straight from req.body, req.params or req.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

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.

Edit this pageSources: cli/internal/sast/rules/vnx-639.rego, cli/internal/sast/docs/vnx-639.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.