Skip to content

Commit

Permalink
[javascript] Object Property Call Linker (#4634)
Browse files Browse the repository at this point in the history
A common pattern in JavaScript is something like

```javascript
var foo = {};
foo.bar = {};
foo.bar.someFunc = function someFunc() {};

foo.bar.someFunc();
```

This PR adds a post-processing pass to find instances where the definition and the call live in the same file and to link them. This "common file" limitation aims to reduce false linking.
  • Loading branch information
DavidBakerEffendi committed Jun 4, 2024
1 parent b93288e commit a23d9fc
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ object JsSrc2Cpg {
List(new JavaScriptInheritanceNamePass(cpg), new ConstClosurePass(cpg), new JavaScriptImportResolverPass(cpg))
++
new JavaScriptTypeRecoveryPassGenerator(cpg, typeRecoveryConfig).generate() ++
List(new JavaScriptTypeHintCallLinker(cpg), new NaiveCallLinker(cpg))
List(new JavaScriptTypeHintCallLinker(cpg), ObjectPropertyCallLinker(cpg), new NaiveCallLinker(cpg))
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.joern.jssrc2cpg.passes

import io.shiftleft.codepropertygraph.generated.nodes.{Call, MethodRef}
import io.shiftleft.codepropertygraph.generated.{Cpg, PropertyNames}
import io.shiftleft.passes.CpgPass
import overflowdb.BatchedUpdate
import io.shiftleft.semanticcpg.language.*

/** Perform a simple analysis to find a common pattern in JavaScript where objects are dynamically assigned function
* pointers. To keep this precise, this will only match objects defined within the scope of the same file.
*
* This relies on JavaScriptTypeHintCallLinker.
*/
class ObjectPropertyCallLinker(cpg: Cpg) extends CpgPass(cpg) {

override def run(builder: BatchedUpdate.DiffGraphBuilder): Unit = {

def propertyCallRegexPattern(withMatchingGroup: Boolean): String =
"^(?:\\{.*\\}|.*<returnValue>):<member>\\(" + (if withMatchingGroup then "(.*)" else ".*") + "\\):.*$"

val propertyCallRegex = propertyCallRegexPattern(true).r
val objectCalls = cpg.call.methodFullName(propertyCallRegexPattern(false)).l
val propertyAccessToCalls = objectCalls
.flatMap { call =>
call.methodFullName match {
case propertyCallRegex(baseProperty) => Option(s"$baseProperty.${call.name}" -> call)
case _ => None
}
}
.groupBy(_._1)
.map { case (k, vs) => k -> vs.map(_._2) }
cpg.assignment
.and(_.source.isMethodRef, _.target.isCall.fieldAccess)
.map { a => a.target.asInstanceOf[Call] -> a.source.asInstanceOf[MethodRef].referencedMethod.fullName }
.foreach { (functionTarget, calleeFn) =>
propertyAccessToCalls
.filter { case (propertyAccess, _) => functionTarget.code.endsWith(propertyAccess) }
.foreach { case (_, calls) =>
calls.where(_.file.nameExact(functionTarget.file.name.toSeq*)).foreach { c =>
builder.setNodeProperty(c, PropertyNames.METHOD_FULL_NAME, calleeFn)
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -463,4 +463,24 @@ class TypeRecoveryPassTests extends DataFlowCodeToCpgSuite {

}

"A function assigned to a member should have it's full name resolved" in {
val cpg = code("""
|var foo = {};
|
|foo.bar = {};
|
|foo.bar.evaluator = function evaluator (src) {
| eval(src);
|};
|
|foo.bar.getGlobals = function getGlobals (src) {
| "use strict";
| var original = Object.keys(global);
| foo.bar.evaluator(src);
|};
|""".stripMargin)

cpg.call("evaluator").methodFullName.head shouldBe "Test0.js::program:evaluator"
}

}

0 comments on commit a23d9fc

Please sign in to comment.