fix(rules): Node vm is an eval sink; a package name is not a sink token

mongo-express CVE-2019-10758 passes the request body to vm.runInNewContext. No
injection sink named that call, so the flow was answered and missed. runInNewContext,
runInContext, runInThisContext and compileFunction are now CWE-95 sinks.

Once the file's questions completed, the fixed pin showed a second defect. Every call
on `require('mongodb-query-parser')` has that package name as its full name. The
bounded-token match treats `-` as a boundary, so the name matched the SQL sink
`query`. In a full name, `-` belongs to a package name, so it no longer separates
tokens.

Exploratory (retune after protocol-v1): mongo-express vulnerable pin now detected at
lib/bson.js:60.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-25 11:47:15 +02:00
co-authored by Claude Opus 5.5
parent dbcb230990
commit ec3d1457fb
3 changed files with 72 additions and 2 deletions
+8 -2
View File
@@ -121,6 +121,12 @@
def mentionsToken(code: String, tokens: List[String]): Boolean =
tokens.exists(t => boundedPattern(t).matcher(code).find())
// A full name is not source text: a `-` in it is part of an npm package's name, never an operator.
// `require('mongodb-query-parser')` gives every call on the parser the full name `mongodb-query-parser...`,
// and read as code that mentions the SQL sink `query` -- mongo-express's fixed pin was reported twice for
// parsing a string with the very library its fix switched to.
def fullNameMentions(fullName: String, token: String): Boolean = mentionsToken(fullName.replace('-', '_'), List(token))
val sourcePatterns = split(sourcesS)
val sinkNames = split(sinksS)
val sanitizerNames = split(sanitizersS)
@@ -314,7 +320,7 @@
def familySinkCall(node: io.shiftleft.codepropertygraph.generated.nodes.AstNode): Boolean = node match {
case c: io.shiftleft.codepropertygraph.generated.nodes.Call if !c.name.startsWith("<operator") =>
val callee = c.code.takeWhile(_ != '(')
sinkNames.exists(n => c.name == n || mentionsToken(callee, List(n)) || mentionsToken(c.methodFullName, List(n)))
sinkNames.exists(n => c.name == n || mentionsToken(callee, List(n)) || fullNameMentions(c.methodFullName, n))
case _ => false
}
@@ -701,7 +707,7 @@
def sinkMatches(c: io.shiftleft.codepropertygraph.generated.nodes.Call, n: String): Boolean = {
val operator = c.name.startsWith("<operator")
c.name == n || (!operator && (mentionsToken(calleeText(c.code), List(n)) || mentionsToken(c.methodFullName, List(n))))
c.name == n || (!operator && (mentionsToken(calleeText(c.code), List(n)) || fullNameMentions(c.methodFullName, n)))
}
// Does THIS path element cleanse the value flowing through it?
@@ -11,6 +11,15 @@ cwe = "CWE-95"
calls = ["eval"]
rule_ids = ["js-eval"]
[[sink]]
# CWE-95. Node's `vm` module compiles and runs a string exactly as `eval` does -- its own documentation says
# it is not a security mechanism. mongo-express CVE-2019-10758 is `vm.runInNewContext('doc = eval((' +
# string + '));')` over a request body, and with only `eval` listed the flow ended at a sink nobody named.
id = "vm.eval"
cwe = "CWE-95"
calls = ["runInNewContext", "runInContext", "runInThisContext", "compileFunction"]
rule_ids = []
[[sink]]
id = "exec"
cwe = "CWE-78"