Efficient Static Vulnerability Analysis for JavaScript with Multiversion Dependency Graphs

While static analysis tools that rely on Code Property Graphs (CPGs) to detect security vulnerabilities have proven effective, deciding how much information to include in the graphs remains a challenge. Including less information can lead to a more scalable analysis but at the cost of reduced effectiveness in identifying vulnerability patterns, potentially resulting in classification errors. Conversely, more information in the graph allows for a more effective analysis but may affect scalability. For example, scalability issues have been recently highlighted in ODGen, the state-of-the-art CPG-based tool for detecting Node.js vulnerabilities. This paper examines a new point in the design space of CPGs for JavaScript vulnerability detection. We introduce the Multiversion Dependency Graph (MDG), a novel graph-based data structure that captures the state evolution of objects and their properties during program execution. Compared to the graphs used by ODGen, MDGs are significantly simpler without losing key information needed for vulnerability detection. We implemented Graph.js, a new MDG-based static vulnerability scanner specialized in analyzing npm packages and detecting taint-style and prototype pollution vulnerabilities. Our evaluation shows that Graph.js outperforms ODGen by significantly reducing both the false negatives and the analysis time. Additionally, we have identified 49 previously undiscovered vulnerabilities in npm packages.


INTRODUCTION
Static analysis tools based on Code Property Graphs (CPGs) [61] have become increasingly popular in recent years.CPGs, as originally introduced for analyzing C/C++ functions, are a graph-based representation that combines Abstract Syntax Trees (AST), Control Flow Graphs (CFG), and Program Dependence graphs (PDG).This rich data structure captures patterns for common vulnerabilities, such as buffer overflows and format strings.Tools that use CPGs start by generating the program's CPG and then iterating through the CPG in search of patterns that indicate potential vulnerabilities.Because search patterns are customizable through graph traversal queries, CPGs offer high flexibility in detecting a broad spectrum of vulnerability types.
CPGs have been adopted for many languages [2,7,10,29,36] and other analytical scopes, including detection of GDPR compliance violations [4,19,48] and data privacy breaches [32].The original CPG data structure has been adapted to fit the specificities of programming languages and analyses.For example, Li et al. [36] developed ODGen, a tool that extends CPGs with an Object Dependence Graph (ODG) to detect vulnerabilities in Node.js applications.ODGen performs the analysis on a combined CPG-ODG data structure, where ODG's nodes represent objects, variables, and scopes, while edges capture relations between them, enabling the detection of taint-style and prototype pollution vulnerabilities.Currently, ODGen is the static vulnerability scanner for npm packages with the most favorable trade-off between effectiveness and precision [6].
Even with these advances, a fundamental challenge in using CPGs for vulnerability detection is deciding what information to include in the graphs so that the definition and identification of vulnerability patterns are both straightforward and precise.On the one hand, including less information limits the number of vulnerability patterns that can be precisely defined on the graph; analysis can either over-approximate, resulting in high false positives, or under-approximate, leading to high false negatives.For instance, using the original CPG alone, without the ODG component, is insufficient for detecting vulnerabilities in JavaScript programs, leading to false negatives.On the other hand, incorporating more program properties into the graph decreases the performance of both graph construction and query execution.Prolonged query execution may result in timeouts, causing the analysis to miss vulnerabilities.This has been observed for ODGen, for which scalability issues have been recently highlighted [27,63].Furthermore, complex graphs that intertwine multiple types of program representation into the same structure are challenging to reason about, resulting in complex vulnerability queries whose correctness guarantees are difficult to ascertain.Particularly, the construction of such graphs follows intricate semantic rules that make it hard to understand the formal properties of the graphs, both in relation to the underlying concrete program semantics and in relation to the true negatives and false positives allowed by the analysis.For example, how can we be sure that if the program has a code injection vulnerability, then the code injection pattern must exist in the constructed graphs?
This paper examines a different point in the design space of CPGs for JavaScript vulnerability detection.In particular, we observe that the AST and CFG contain a lot of extraneous information not needed for vulnerability detection.Furthermore, we note that with the current ODGs [27,36,63], vulnerability queries must jump back and forth between the CPG and the ODG of the given program in search of complex graph patterns that are not only difficult to specify and reason about, but also degrade the performance of the analysis.We ask the question: can we design one unified graph to capture all the essential information for common JavaScript vulnerability detection?
To this end, we introduce Multiversion Dependency Graphs (MDGs), an efficient graph-based data structure for statically detecting common vulnerabilities in JavaScript programs, which encompasses two types of analysis: (1) classical shape analysis [25,51], where each object is mapped to the set of properties it may have during execution; and (2) dependency analysis [33,47], associating the objects and values created during execution with the values on which they depend.One key insight of our work is that the graph and query complexity can be reduced by relying on a single graph that models the evolution of objects and properties of the given program over time.Specifically, we create new versions of objects and properties each time an object is updated.Version information allows us to keep track of both data flows and the execution order in a single graph (more in §2), greatly reducing graph and query complexity.A second insight is that using a summary fixed-pointed representation for loops and recursive function calls reduces graph and query complexity without increasing false positives in almost all vulnerability detection tasks in our datasets.To understand the formal properties of the constructed MDGs, we formalize our MDG construction algorithm and prove that it is sound, i.e., the generated MDGs are an over-approximation of the concrete execution traces.This implies that if a program has a vulnerability that can be detected through the analysis of the program trace, then the corresponding vulnerability pattern occurs in the generated MDG.
To evaluate the effectiveness of our approach, we implemented Graph.js 1 , a static vulnerability scanner for JavaScript code.Graph.jsfocuses on analyzing npm packages from the Node.jsecosystem, known to have numerous vulnerabilities [1,6,53,64].To analyze a package, Graph.jsgenerates its MDG, which it then stores in a Neo4j graph database.Graph.jsproceeds to detect vulnerabilities by executing specific queries written in Cypher.Currently, Graph.js can detect three different kinds of taint-style vulnerabilities as well as prototype pollution vulnerabilities.
Our evaluation of Graph.js on two curated datasets [5,6] shows that our tool significantly outperforms ODGen, the state-of-the-art tool, with lower false negatives and shorter analysis time.In particular, Graph.jsdetects 82% of the reported vulnerabilities in the ground truth datasets, surpassing ODGen by 1.63×, with 1.23× the precision.On average, Graph.jscompletes its analysis of 603 packages within 4.61 seconds and can analyze 95% in under 10 seconds.In 99% of the cases, Graph.js'sMDG are smaller than the ODGs generated by ODGen, with only 0.14× the nodes and 0.42× the edges.Moreover, with Graph.js,we have identified 49 previously undiscovered vulnerabilities in npm packages, which we have responsibly disclosed to the package developers.

MOTIVATION AND OVERVIEW
In this section, after briefly reviewing CPGs, we introduce a motivating example of a vulnerable JavaScript code ( §2.1) and provide an overview of our proposed approach ( §2.2).
In general, CPG-based vulnerability detection approaches have important advantages.(1) Generality and modularity: The graph serves as a universal structure for detecting a spectrum of vulnerabilities; variations in vulnerabilities are addressed in the query phase, allowing graph reuse and eliminating overhead in graph reconstruction.(2) Compositionality: Code changes only require partial reconstructions of the CPG and rerunning pertinent queries instead of a full-scale analysis.
To facilitate tracking dependencies across objects and properties in JavaScript, ODGen augments a program's CPG with another data structure called the Object Dependency Graph (ODG) [36].ODG's nodes can represent variables or objects.Between the CPG and ODG, a total of seven types of edges are used, including object definition edges for linking objects to the AST node where the object was declared; data flow edges for connecting one object to another; property edges for associating properties with objects; and AST-OBJ lookup edges for linking nodes between CPG and ODG.ODGen has been shown to be effective in detecting many Node.jsvulnerabilities [27,36].

Motivating Example
Figure 1a presents an exemplary vulnerable JavaScript code sample that offers insight into how code property graphs can be used to detect vulnerabilities.The git_reset function (lines 3-9) initiates a  Taint-style vulnerability: Occurs when untrusted input from a given source reaches a vulnerable sink without undergoing proper validation or sanitization, potentially leading to malicious exploitation or unintended behavior.Specifically, the program in Figure 1a contains an exploitable command injection vulnerability, i.e., by using the payload shown in Figure 1d, an attacker can prompt the exec function to run the command 'git reset HEAD∼1 | rm -rf /', which deletes all local files.In general, to detect such vulnerabilities, the analysis needs to perform the following steps: (1) identify potential unsafe sources (e.g., function inputs) and unsafe sinks (e.g., exec) and (2) determine whether tainted inputs from sources can reach the sinks.At a high level, CPGs enable (1) searching through AST for known sink functions, and (2) tracking data dependency information between the untrusted sources (tainted inputs) and arguments used by the sink function.
Prototype pollution vulnerability: It arises when an attacker manages to manipulate the prototype of an object, leading to side effects such as Denial-of-Service (DoS) or arbitrary code execution.Figure 1e illustrates that a prototype pollution vulnerability within the git_reset function can be exploited to induce a DoS by substituting JavaScript's built-in toString function with the function referenced by the variable malicious_fn.The next invocation of toString will enter the infinite loop of malicious_fn, causing the program to hang.The prototype is polluted on lines 4 and 5 of Figure 1a.At a high level, to detect prototype pollution, CPG-based approaches need to collect the following information: (1) the location where the prototype pollution happens, i.e., an object lookup followed by an object assignment over the initial lookup (e.g, lines 4 and 5); and (2) whether tainted inputs from sources can reach both the properties and the value assigned (op, branch_name and url respectively).The former can be obtained from the AST and the latter needs dependency analysis, similar to the previous example.

Overview of MDGs
To design a new and simpler CPG data structure for JavaScript, we first narrow down the following vulnerabilities: prototype pollution (CWE-1321 [56]), and taint-style vulnerabilities, which include OS command injection (CWE-78 [58]), arbitrary code execution (CWE-94 [59]), and path traversal (CWE-22 [57]).These vulnerabilities include representative information that CPGs should capture.We do not foresee technical difficulties in handling other vulnerabilities outlined in prior work [36], as they use similar information.The information needed for identifying the above-mentioned vulnerabilities includes: data dependencies between variables and objects, sequences of read/write operations to objects and their properties, and a mapping between variables used in the source code and heap objects/values that exist at runtime.We are able to use one graph, Multiversion Dependency Graph (MDG), to capture all of these.Next, we present our MDGs and associated queries using the example in Figure 1a.
Multiversion Dependency Graph (MDG), a simpler graph: The MDG for the function git_reset is shown in Figure 1c.The graph is generated by abstractly executing the program line by line.
The MDG consists of two types of nodes: objects and function calls.Object nodes represent objects or primitive values computed during the execution of the program.Each node has a label   :  , where   identifies a specific object version, and   denotes the name of the variable (or variables) in a given line of the source code pointing to the node's object or value.Nodes in each gray box are typically objects whose properties are accessed while the line of code indicated by the line number above the gray box is abstractly executed.For instance, node  5 , created during the analysis of line 4, refers to an object pointed to by: (i) the options variable and (ii) a property (op) of the object referenced by the config variable.We will explain the wildcard * notation later.Function call nodes, denoted as   :  (), represent the invocations of a function   () in a specific line and are identified by the label   .Figure 1c includes node  1 corresponding to the exec() function call on line 7.
MDGs have three types of directed edges: property edges, version edges, and dependency edges, and grows as the program is abstractly executed.Next, we detail how the graph in Figure 1c is generated.Our analysis is applied to one statement at a time in a forward manner, with the MDG being updated as the analysis proceeds.
• Line 4: First, the analysis identifies a property lookup, config [op].As the object that represents config ( 1 ) has no known property, the analysis lazily initializes a new property in  1 , creating a new node  5 for the accessed property op, and then adding a property edge  1 P(*) −− → 5 , meaning that  5 is a property of  1 .Since the property name is not known at static time, the property edge is labeled with '*'.Additionally, the analysis creates a dependency edge  2 D − → 5 , indicating that this dynamic property's name depends on the value of the variable op ( 2 ).Finally, the analysis updates the value of the program variable options to the newly created object  5 .−−−−−− → 9 , because although the algorithm is only reading the property now, it existed from the beginning.Then, similarly to Line 4, the analysis returns the object nodes that contain property commit.However, in this case, it will find two versions:  9 , because  5 is the latest version that contains property commit; and  4 , because a more recent version of the object ( 6 ) has a dynamic property, which may have overwritten property 'commit'.Having evaluated both expressions, the analysis creates three dependency edges from the resulting nodes to the node representing the function call,  8 As shown above, the MDG captures how objects evolve over time as their properties are updated; these updates are captured by new version edges.For instance, object  6 , referenced by the variable options, is a new version of object  5 , resulting from the update of the object's dynamic property in line 5. Object updates can be identified by these version edges.Property edges  (), where  is the property name, capture the internal structure of objects during the program's execution.They denote that the object pointed to by the edge is a sub-object of the object version from which the edge originates.For example, the property edge  1 P(*) −− → 5 indicates that  5 is a sub-object of object version  1 derived from the dynamic property op (line 4).The dependency edges  denote data dependencies between values/objects in two cases: (i) when a sub-object is looked up (read) by a property name, as seen in dependencies  2 MDG queries: MDG allows for simpler and more efficient query specifications for vulnerability detection.Firstly, the order between operations delineated by writes can be easily determined due to the tracking of multiple versions of objects.Specifically, given a version edge   V(p) −−→  , we know that the instruction on line line  that led to the creation of object version   is a write operation and that it was executed after the instruction on line line  that resulted in the prior object version   .This characteristic facilitates the identification of prototype pollution vulnerabilities.For example, a typical instance of prototype pollution, such as the one present in the vulnerable function shown in Figure 1c, occurs when there is an object lookup in a property  1 , followed by an assignment of a value  to a property  2 of the obtained sub-object, where an attacker controls  1 ,  2 , and .The MDG in Figure 1c  tainted lookup dependent on  2 , followed by a tainted property update dependent on  3 and  4 .The sequential ordering is captured by the version edge  5 V(*) −−→ 6 .Secondly, the MDG is self-contained, avoiding the need for costly AST and CFG visits.Specifically, the MDG's edge types encapsulate essential information, allowing data flows to be tracked through simple graph traversals across these edge types.This characteristic is particularly beneficial for detecting taint-style vulnerabilities.For instance, in Figure 1c, once we establish that the sensitive sources are the git_reset's input parameters (i.e.,  1 ,  2 ,  3 , and  4 ), and the sink is the exec function call node (i.e.,  1 ), it becomes apparent that the graph includes paths from all tainted inputs reaching the sink.For example, we can trace the sequence of dependencies from  1 using

SOUND MDGS FOR JAVASCRIPT
In this section, we formally define Multiversion Dependency Graphs (MDGs) ( §3.1) and an abstract interpretation-based analysis that computes them for a core of JavaScript ( §3.2).We then present a soundness theorem that establishes the guarantees of the proposed analysis ( §3.3).A complete account of the analysis, including the proof of its soundness, can be found in [21].

Syntax of Multiversion Dependency Graphs
A Multiversion Dependency Graph, denoted ĝ = ( V , Ê) ∈ Ĝ, tracks the structure and evolution of objects during the program execution and the data dependencies between the values that the program manipulates.The graph nodes, V , are taken from the set of abstract locations, l ∈ L, and represent objects and primitive values computed during program execution.In the MDG of the running example in Figure 1c, these abstract locations are represented with the label   .The graph edges Ê, taken from the set of labeled edges, connect pairs of abstract locations.Each edge is annotated with its type, a label , given by the grammar: Edges labeled with D are Dependency Edges.An edge l1 ↦ → D l2 means that the value/object represented by l2 is computed using (depends on) the value/object represented by l1 .For instance, l2 ↦ → D l5 in Figure 1c resulted from the property lookup on line 4, where the property name depends on the value of variable op.Edges labeled with P()/P( * ) are Property Edges.A known-property edge l1 ↦ → P( ) l2 means that the object represented by l1 has a property named  mapped to a value represented by l2 .An unknown-property edge l1 ↦ → P( * ) l2 has the same meaning as the known one, except that the property name cannot be determined statically.For instance, l1 ↦ → P( * ) l5 in Figure 1c comes from the property lookup on line 4, where the property is represented by the variable op.Edges labeled with V()/V( * ) are Version Edges.A known-property version edge l1 ↦ → V( ) l2 means that the object represented by l2 is a new version of the object represented by l1 , resulted from an update of its property .An unknown-property version edge l1 ↦ → V( * ) l2 has the same meaning as the known one, except that the name of the updated property is not known at static time.The version edge l5 ↦ → V( * ) l6 in Figure 1c results from an update of a dynamic property on line 5.
In the following, we write ĝ[ l, ] to denote the set of abstract locations associated with the object represented by l via property .These locations may be directly connected to l via a property edge labeled with P() or connected to a previous version of l, as we do not duplicate properties that are not updated when creating a new version of an object.For instance, Figure 1c, ĝ[ l7 , ] = { l8 }.The abstract graph over-approximates the program's concrete heap state, so ĝ[ l, ] may contain more than one abstract location, for instance, branches of an if statement updating an object differently, would result in different object versions at the join of the branches.For instance, in Figure 1c, ĝ[ l7 , ] = { l4 , l9 }.

Computing Multiversion Dependency Graphs
We formalize our analysis for computing MDGs for a core of JavaScript.
Core JavaScript syntax: Our core of JavaScript includes expressions and statements, shown below.Expressions  ∈ E include values and program variables.Statements  ∈ S include: assignments, binary operations, property lookups, property assignments, new object creation, if and while statements, sequencing, and function calls.Each statement that computes new values or objects has a unique index , which we explain later with analysis rules.
Abstract variable store: To connect program variables to the objects that they represent, we use abstract variable stores ρ ∈ S : X ⇀ ℘( L) that map program variables to sets of abstract locations, where ρ () denotes the set of abstract locations that  represents.Given an expression  and an abstract store ρ, the evaluation of  under ρ, written ⟦⟧ ρ , denotes the set of abstract locations that  represents.For instance, ⟦options⟧ ρ = { l8 } in the MDG of Figure 1c.Observe that ρ () only contains the newest versions of the objects associated with .Abstract stores form a lattice under the standard pointwise subset inclusion; formally: a store ρ1 is said to be lower than or equal to a store ρ2 , written ρ1 ⊑ ρ2 , if and only if dom( ρ1 ) ⊆ dom( ρ2 ) and ∀ ∈ dom( ρ1 ).ρ1 () ⊆ ρ2 ().Focusing only on the dependency aspect of the graph, we can say a graph/store is lower than another if it contains fewer dependency edges.If the sets of abstract locations and program variables are finite, then so are the lattices of MDGs and abstract stores.
Auxiliary graph functions: Next, we explain two auxiliary functions used in graph construction.
The function NV  ( ĝ, ρ,  1 ,  1 ) is used to create a new version of objects represented by locations in  1 , due to an assignment to property  1 .Here  is the index of the statement where this function is called.It returns a tuple ( ĝ′ , ρ′ ,  ′ 1 ), where ĝ′ is the updated graph,  ′ 1 is the set of abstract locations representing newly created objects, and ρ′ is the updated store with the occurrences of older version locations replaced by their corresponding newer versions.
The function AP  ( ĝ, ,  1 ) extends a set of objects represented by locations in  with a property edge P( 1 ) and returns the new graph ĝ′ .The index  has the same meaning as above.If a location l1 in  already has a property edge labeled as  ( 1 ), then no action is taken; otherwise, a new location l2 is allocated and l1 ↦ → P( 1 ) l2 is added to the graph.
The above functions have an alternate version for properties whose values are computed dynamically.For example, on line 4 of the program in Figure 1a, the property name (op) is non-static.In this case, AP *  ( ĝ,  1 ,   ) extends each object in  1 with an unknown-property edge pointing to an abstract location that depends on all locations in   , where   is the set of abstract locations that represent the non-static property .If a location l1 in  1 does not have a property edge labeled as  ( * ), then a new location l2 is allocated and l1 ↦ → P( * ) l2 is added to the graph; otherwise, the dependencies in   are added to the existing property.Analogously, NV *  ( ĝ, ρ,  1 ,   ) creates a new version of all objects corresponding to locations in  1 , making each new object depend on all locations in   .These dynamic versions of the rules ensure that locations denoting dynamically Assign-Op computed properties are connected to the updated/looked-up objects via dependency edges; these are essential for effective taint propagation.
Analysis rules for graph construction: We formalize our graph construction using a declarative function A. We write A (, ĝ, ρ) = ( ĝ′ , ρ′ ) to mean that the analysis of statement  starting from the initial abstract state ( ĝ, ρ) results in the final abstract state ( ĝ′ , ρ′ ).Selected rules for A are shown in Figure 2. The rule [Assign-Op] evaluates both expressions,  1 and  2 , and creates a new location, l , representing the result of the binary operation; this new location is then set to depend on all the locations to which  1 and  2 evaluate and the variable  is set to the singleton set containing { l } in the abstract store.The [New Object] rule generates an abstract location for the created object, calling the function alloc with the unique identifier  and the current graph ĝ.An abstract allocation does not necessarily generate a fresh abstract location; we choose to always generate the same abstract location for the same literal object.This means that objects created within a loop are represented by the same abstract location, avoiding object explosion.The [Static Property Lookup] rule evaluates the expression  denoting the object being inspected, obtaining a set of locations ; it then uses the function AP to extend the objects in  with the property  in case they do not define it; finally, it obtains the set of locations  ′ representing the values of property  in the objects in  and sets the variable  to  ′ in the abstract store.The [Dynamic Property Lookup] Rule (omitted) is analogous, except that AP * is used with an additional argument corresponding to the set of locations representing the dynamic value of the property.
Figure 3 illustrates the graphs and stores after applying the analysis rules for lines 4 and 5 of the running example in Figure 1a.The graph on the left is a subgraph of Figure 1c.The abstract stores ρ represent the content of the abstract store ρ after analyzing line .The edges in blue are generated as a result of analyzing line 4 and those in red are generated when analyzing line 5.
The rule [Dynamic Property Lookup] is used when analyzing line 4 of the example.Here  = options :=  config[op].First, config and op evaluate to { l1 } and { l2 }, respectively.Then, the rule calls AP *  ( ĝ, { l1 }, { l2 }), which (1) extends l1 with the dynamic property * , represented by the abstract location l5 , via edge l1 ↦ → P( * ) l5 , and (2) adds a dependency edge l2 ↦ → D l5 , as the property being looked up depends on the value of l2 .Finally, it sets the variable options to l5 in the store.
The [Dynamic Property Update] rule evaluates the expressions  1 ,  2 , and  3 , respectively denoting the object being updated, the property to be updated, and the assigned value, obtaining three sets of locations  1 ,  2 ,  3 ; then, the rule uses the function NV * to create a new version of all the objects  The [If] rule evaluates both branches of the if statement and combines the results using the least upper bound operator.Finally, the [While] rule computes the least fixed point of the analysis on the body of the loop.When analyzing a loop while(){} on a state ( ĝ, ρ), we must find the smallest state ( ĝ′ , ρ′ ) such that: ( ĝ, ρ) ⊑ ( ĝ′ , ρ′ ) and A (, ĝ′ , ρ′ ) = ( ĝ′ , ρ′ ).Such fixed point is guaranteed to exist because the set of abstract states forms a finite lattice and the analysis is monotone: for any ĝ1 and ĝ2 , ρ1 and ρ2 , and statement , it holds that: ( ĝ1 , ρ1 ) ⊑ ( ĝ2 , ρ2 ) =⇒ A (, ĝ1 , ρ1 ) ⊑ A (, ĝ2 , ρ2 ).

Soundness
To better understand the formal properties of our MDG, we first define a concrete semantics for the core language and then show that the MDGs generated by our analysis overapproximate the concrete object layout and structure in the concrete semantics.At a high level, the property established here guarantees that our graph generation algorithm consistently handles dynamic properties such that the abstract graph does not miss any edges that the concrete one generates.
Instrumented concrete semantics: Similar to the abstract store used in the analysis semantics, we define concrete stores,  ∈ S : X ⇀ L, to map program variables to locations.To track the values computed during execution, we use heaps ℎ ∈ H : L ⇀ V that map locations to values.Compared to directly using values and variables, using the store simplifies dependency tracking.Object structure and dependencies are modeled through the concrete multiversion dependency graphs,  = ( , ) ∈ G, which are analogous to their abstract counterparts in all respects except that nodes are taken from the set of concrete locations,  ⊆ L, all edge types are known, and any given location  can only be connected to at most one other location via a given property edge P().
Figure 5 shows selected rules for the instrumented big-step semantics of Core JavaScript.The semantics rules take the form ⟨, ℎ, , ⟩ ⇓ c ⟨ ′ , ℎ ′ ,  ′ ⟩, meaning that the evaluation of statement  in the initial concrete MDG , heap ℎ, and store , yields the final graph  ′ , heap ℎ ′ , and store  ′ .
Analogously to the abstract semantics, the concrete semantics also keeps track of the entire history of the program execution by creating a new version of each object whenever it is updated.
The [Dynamic Property Lookup] Rule evaluates the expressions  1 ,  2 , respectively denoting the object and property being looked up, and obtains the locations  1 and  2 .Then, it obtains the location  ′ denoting the property value and updates the store accordingly.Finally, it adds a dependency edge  2 ↦ → D  ′ , as the property being looked up depends on the value of  2 .The [Dynamic Property Update] Rule evaluates the expressions  1 ,  2 , and  3 , respectively denoting the object being updated, the property to be updated, and the value assigned to that object, obtaining three locations  1 ,  2 ,  3 ; then the rule extends the graph with a new version of the object represented by  1 and the edge  1 ↦ → D  ′ .Analogously to the abstract counterpart, it adds a property edge corresponding to the property being assigned ( 3 ) to the new version object ( ′ ).The NV  function is similar to NV  function, except that the property name is resolved to a static value, instead of the wildcard '*'.
Similarly to Figure 3, Figure 4 illustrates the concrete sub-MDG, resulting from evaluating the first five lines of the example of Figure 1c, where config is the object { reset: {}}, and op, branch_name and url are strings 'reset', 'main', and 'origin/main', respectively.The concrete stores   represent the content of the concrete store  after analyzing line , and the heaps ℎ  represent the content of the heap ℎ after analyzing line .In contrast to the abstract sub-graph of Figure 3, since we know the object's structure and values in the initial state, line 4 only adds a dependency edge  2 ↦ → D  5 , also mapping the variable options to  5 .Line 5 contains a dynamic property update, and, similarly to the abstract sub-graph, we create a new version of the object represented by  5 , and extend the newly created version ( 6 ) with a property edge pointing to  4 .However, as we know the value of the property being updated, instead of using the wildcard '*', we use the property name 'main'.

Analysis guarantees:
We establish that the abstract MDG overapproximates concrete MDG.We define abstraction functions,  : L ⇀ L, that map concrete locations to abstract locations; i.e.,  () = l means that the concrete location  is represented by l in the abstract domain.Intuitively, an abstract store ρ over-approximates a concrete store  according to an abstraction function  if all the variables in the domain of  are over-approximated by ρ.Analogously, an abstract graph ĝ over-approximates a concrete graph  if all the edges of  have corresponding edges in ĝ.Definitions 3.1 formalizes the relation between abstract graphs and concrete graphs.

Definition 3.1 (MDGs Over-Approximation
).An abstract MDG ĝ is said to over-approximate a concrete MDG  via abstraction function , written ĝ ∼  , if and only if the following hold: In the following, we write ĝ, ρ ∼  ,  to mean that ĝ over-approximates  and ρ over-approximates  according to .Theorem 3.2 states that if the initial abstract state over-approximates the initial concrete state, then the final abstract state also over-approximates the final concrete state.

GRAPH.JS
To validate our approach, we implemented Graph.js, a novel static vulnerability scanner for JavaScript code, based on MDG graphs.Here, we describe the graph queries performed by Graph.js to detect taint-style and prototype pollution vulnerabilities and the implementation of Graph.js.

Basic graph traversals:
A graph traversal, as proposed by Yamaguchi et al. [61], is a function T : P (V) → P (V), that maps a set of nodes to another set of nodes, where V is a set of nodes and P (V) is the power set of V.This definition allows for chaining multiple traversals together, e.g., T 0 • T 1 represent two graph traversals T 0 and T 1 chained together, using a function composition •.It also allows for filtering traversals, e.g., T 0 \T 1 represent a graph traversal T 0 , excluding the paths included in T 1 .
Table 1 defines elementary traversals, which are the building blocks for more complex traversals for finding vulnerabilities (summarized in Table 2).We first define BasicPath   , which finds a path between   and   .For instance, the MDG of Figure 1c contains a basic path between  1 and  8 , presented as  1 The other traversals are built upon this notion, adding more restrictions to the path.For instance, TaintedPath   returns all paths returned by BasicPath   , except those that are also included in UntaintedPath   (explained later in this section).In Table 2, Sources represent untrusted input, e.g., user input, and Sinks  represent functions that are classified as unsafe sinks for vulnerability of type .The list of Sinks considered by Graph.js can be set dynamically via a configuration file, where each sink is defined by a JavaScript native function or a function imported from an external package  , and the sensitive argument(s) .Taint-style vulnerability queries: The detection of the three taint-style injection vulnerabilities, i.e., code injection, command injection, and path traversal, share the same graph traversal pattern and differ from each other only in their unsafe sink functions.Code injection vulnerabilities, which consist of injecting code that is later executed by the server, can involve sinks such as eval and Function().The sinks of command injection vulnerabilities, which entail executing arbitrary commands in the server's operating system, include exec, child_process.spawn, and child_process.execFile.Path traversal vulnerabilities, which allow for accessing restricted files on the server by injecting malicious input, use sinks such as fs.readFile and fs.createReadStream.
Graph.js detects such scenarios by searching for paths connecting the tainted source to an unsafe sink in the MDG of the program being analyzed.The traversals for each taint-style vulnerability are shown in Table 2. First, Graph.jsperforms a graph traversal TaintPath  in the MDG, which returns all paths that start in   but excludes untainted paths.Untainted paths contain a new version edge V(prop) followed by an object lookup of the same property P(prop); this pattern indicates that that (tainted) property has been overwritten and is no longer tainted through that path.This traversal is chained with Arg   to return tainted paths that end in argument  of an unsafe function call  .Prototype pollution vulnerability queries: A prototype pollution allows an attacker to manipulate the prototype of an object.In this work, we focus specifically on Object.prototype,which is the topmost object on every prototype chain.Graph.js'sprototype pollution queries search for an object lookup where the attacker controls the property, followed by an object assignment over the result of the initial lookup where the attacker controls both the property and the value assigned.Table 2 shows the traversal for prototype pollution.In a first step, Graph.jsperforms a graph traversal ObjLookup *  chained with ObjAssignment * , , , which together search for an object lookup (  −− →   ).This traversal is then chained with three TaintPath   , that, similarly to taint-style vulnerabilities, check if the attacker controls   ,   and   , sequentially.We can identify this pattern in Figure 1c, where there is an object lookup, followed by an object assignment over the result of the initial lookup, by identifying the pattern  1 Implementation: Graph.jstakes npm packages as input and reports potential vulnerabilities.It is composed of two processing pipelines: MDG generator and graph engine.The MDG generator is implemented with 6K lines of TypeScript code and is responsible for parsing and transpiling JavaScript programs to the core JavaScript and then producing the corresponding MDG.Graph.jsuses Esprima v4.0.1 [14] for parsing before generating the program's AST and CFG in line with the original CPGs introduced by Yamaguchi et al. [62].Then, the MDG builder creates the MDG.The query engine consists of 500 lines of Python code and is responsible for importing the MDG into a graph database and executing a set of queries on the MDG.We used Neo4j v4.2.1 [40] as the graph database engine and wrote two Cypher [39] queries with 80 lines of code, one for the taint-style vulnerabilities and the other for the prototype pollution.

EVALUATION
In this section, we evaluate the effectiveness and performance of Graph.js against npm packages.Specifically, our evaluation aims to answer the following three central research questions: • RQ1: How effective is Graph.js in detecting vulnerabilities and how does it compare to ODGen?
• RQ2: Can Graph.jsfind zero-day security vulnerabilities in real-world npm packages?
• RQ3: What is Graph.js'sperformance and how does it compare to ODGen?

Experimental Setup
To answer our research questions, we leverage three datasets.Two of these, are complementary vulnerability datasets from prior work which we use as ground truth: VulcaN [6] and SecBench [5].
They have different vulnerability distributions over vulnerability types, summarized in Table 3.
Combined, these two reference datasets provide a comprehensive set of vulnerabilities for our evaluation.The third dataset, which we call Collected, is a set of popular packages (>2K weekly downloads) that we downloaded from the npm repository.Next, we describe these three datasets.
• Dataset 1 (VulcaN): VulcaN [6] is a vulnerability benchmark with 957 npm package versions that contain confirmed Node.js vulnerabilities, reported in the GitHub Advisory Database [22].Each package contains one or more vulnerabilities, each of which is annotated with the sink and source line number.Out of the 957 packages, we selected all 174 that contain vulnerabilities that Graph.jstargets: code injection, command injection, path traversal, and prototype pollution.These selected packages contain a total of 236 vulnerabilities.Out of the 236 vulnerabilities, we excluded 17 that either have incorrect annotations (e.g., the annotated vulnerability type is different from the correct type and the correct type is outside our scope) or are located in an external imported package, whose source is unavailable for analysis.• Dataset 2 (SecBench): SecBench [5] comprises 601 vulnerable packages, reported in the GitHub Advisory Database [22], Snyk [49], and Huntr.dev[24].Each package only includes a single vulnerability and is annotated with the sink line number.Out of the 601 vulnerabilities, we selected a total of 384.We excluded 217 vulnerabilities in total: 98 refer to out-of-scope ReDoS vulnerabilities, 71 are incorrectly annotated (e.g., non-existent or wrong sink line, or missing files), and 38 were already included in VulcaN.The rest of the excluded cases either are unavailable for download or their file type was TypeScript.While our methodology applies to TypeScript, Graph.jsuses a JavaScript parser that cannot handle TypeScript.• Dataset 3 (Collected): Contains 32,137 (∼32K) popular real-world npm packages, that we crawled from the npm repository in September, 2023.Following Snyk's guidelines, we consider a package popular if it had more than 2,000 weekly downloads at the time of collection.
To compare Graph.js with prior work, we set up the open-source implementation of ODGen [36] and run it on our ground truth datasets.We chose ODGen because it is the state-of-the-art CPGbased vulnerability detection tool for npm packages.We evaluated Graph.js on the same set of vulnerability types for which ODGen was evaluated.Similarly to ODGen, our evaluation does not include XSS and SQL injection vulnerabilities because the identification of these types of vulnerabilities relies on application-specific sinks.Furthermore, Brito et al. [6], the authors of VulcaN, present an empirical study for evaluating JavaScript vulnerability detection tools on npm packages, and elects ODGen as offering the most favorable trade-off between effectiveness and precision, ranking highest in precision and fourth in overall effectiveness among the assessed tools.Our testbed consisted of 6 64-bit Ubuntu 22.04.3 servers with 64GB of RAM and 2x Intel(R) Xeon(R) Gold 5320 2.2GHz CPUs.We conducted the experiments involving the reference datasets on a single server.To analyze the Collected dataset, we used six servers to distribute the load and speed up the analysis.We set the total analysis timeout to five minutes.

RQ1: Effectiveness in Vulnerability Detection
We assess Graph.js'seffectiveness in detecting vulnerabilities and compare it to ODGen by running both tools on our ground truth datasets: VulcaN and SecBench.Results are summarized in Table 4.

True positives (TP):
We consider a reported vulnerability a true positive if the vulnerability type and sink line number reported by the tools match the dataset annotations.For ODGen, a report is also considered a true positive if it only correctly detects the vulnerability type but does not pinpoint the sink code line 2 .We include all vulnerabilities reported by ODGen until it times out.
The columns titled "TP" in Table 4 show that Graph.js can detect 1.63× more vulnerabilities than ODGen, identifying 494 versus 304 vulnerabilities.In particular, Graph.jsfinds twice as many code injection vulnerabilities (CWE-94) and three times as many prototype pollution vulnerabilities (CWE-1321) as ODGen.The improvement is largely attributable to Graph.js building simpler graphs (as detailed in §5.4), enabling it to complete analysis more quickly than ODGen.In 95% of the cases, ODGen timed out without detecting any vulnerability, struggling particularly to recognize prototype pollution patterns.A contributing factor is that ODGen's abstract interpretation often fails to complete analysis of prototype pollutions involving recursion and loops.In §5.5, we present a case study of a prototype pollution vulnerability where Graph.js'sversion edges and summary fixed-pointed representation for loops enable a speedy detection, whereas ODGen times out.
Regarding the specific vulnerabilities each tool can identify, Figure 6 reveals that the set of vulnerabilities detected by Graph.jslargely subsumes those found by ODGen.Apart from 17 vulnerabilities detected exclusively by ODGen, Graph.jsidentifies all other vulnerabilities that 2 We saw many instances of such reports from ODGen.Since the lack of information about the sink may be an issue related to the implementation, not the approach itself, such a report is credited as a true positive.Thus, our reported TP for ODGen is a conservative upper bound.ODGen detects, i.e., 94%.The reasons for Graph.jsmissing said vulnerabilities are similar to those for false negatives in general.The main reason for false negatives in Graph.js is the set of unimplemented JavaScript functionalities not represented by the MDG, resulting in missing dependency edges in the graph.Currently, MDGs do not provide full support for the arguments and the this keywords, some array operations, and Function.prototype.call().When compared to taint-style detection, Graph.jsexhibits a lower TP rate in prototype pollution detection.This lower rate is partly because prototype pollution patterns often involve third-party npm packages, such as for-own and for-in, which are instrumental in leading to vulnerabilities.However, since the code of these external packages is not represented in the MDG, the prototype pollution query fails to recognize the associated vulnerability pattern.Additionally, prototype pollution sources frequently use the arguments keyword, which, as noted above, is not fully supported by MDGs.
False positives (FP) and true false positives (TFP): We consider a false positive (FP) when a vulnerability is reported by a tool but was not annotated as such in the original dataset.However, it is important to note that both tools often discovered additional confirmed vulnerabilities not annotated in the datasets.This occurrence is not unusual, given that the datasets are not complete.For instance, SecBench reports only one vulnerability per package, yet it is common for vulnerable packages to contain multiple exploitable unsafe sinks; e.g., CVE-2019-10783 describes three exploitable sinks for lsof_v0.1.0while SecBench only reports one.Therefore, in our classification, we specifically designate a result as a true false positive (TFP) only when it does not correspond to an actual, exploitable vulnerability for which we have been able to generate a successful exploit.
Table 4 presents both of these metrics under the columns "FP" and "TFP".Although Graph.js reports a higher number of false positives than ODGen (339 versus 220, respectively), the datasets are incomplete.Consequently, a false positive identified by a tool could be a real, unannotated vulnerability in the dataset, making it a true positive.To account for potential inaccuracies in false positive reporting due to incomplete dataset annotations, we focus on the TFP metric.Analyzing this metric reveals that Graph.jsoutperforms ODGen by reporting 37 fewer true false positives.
In Graph.js, the main causes for TFPs are as follows.For taint-style TFPs, a tainted value reaches an unsafe sink, but it only occurs under highly specific circumstances, which prevent a successful exploitation of the vulnerability.In the case of prototype pollution TFPs, this issue arises because our graph traversals do not evaluate if conditions, which leads to the reporting of recursive object assignments as sinks, even if cases where the if condition is not executed.While these assignments may contribute to the vulnerability's existence, they do not directly pollute Object.prototype.
In comparison, ODGen has no TFPs in path traversal .This is because ODGen uses very specific queries that only search for the unsafe sinks in the context of a web server, i.e., the tainted path must pass in functions CreateServer or CreateHttpServer.For prototype pollution vulnerabilities, ODGen has only 13 TFPs, though it also has a low TP rate, as discussed above.
Precision, recall, and F1-score: Table 4 also presents the precision, recall, and F1-score of both Graph.js and ODGen.Precision is computed as  /(  +  ), where TP only includes the annotated vulnerabilities.Recall is calculated as  /(  +   ).F1-score is determined using the harmonic mean of precision and recall: (2× Precision × Recall)/(Precision + Recall).Globally, Graph.jsachieves a precision of 78%, which is an increase of 14 points over ODGen's precision.The most significant improvement of Graph.js over ODGen is observed on the recall, which rises from 50% to 82%, respectively, playing a decisive role in boosting Graph.js'sF1-score by 1.42× that of ODGen.

RQ2: Vulnerability Detection in the Wild
We assess Graph.js'capability to detect zero-day vulnerabilities, by applying Graph.js to analyze the 32K npm packages from the Collected dataset.We define a vulnerability as zero-day if (1) a human expert confirms the vulnerability with a generated exploit, (2) we cannot find any information about the vulnerability online, and (3) it is not an intended functionality of the package.Table 5 summarizes our results.Initially, Graph.js reported 2,669 vulnerabilities (see the column "Reported").From those, we randomly sampled 396 packages to manually analyze, corresponding to 419 vulnerabilities (column "Checked").To limit the manual effort, we prioritized analyzing packages with less than 10 files and code injection and command injection vulnerabilities, as they typically are simpler, making it easier to create the exploits to confirm the vulnerabilities, per our prior experience.From the 419 vulnerabilities we manually checked, we successfully created an exploit for 101 of them (column "Exploitable"); 49 of them were not previously reported and were not an intended functionality of the package.
We detected 318 non-exploitable vulnerabilities (i.e., false positives), mainly due to the presence of sanitization functions between tainted sources and unsafe sinks, or tainted data reaching unsafe sinks only under highly specific circumstances, making the creation of an exploit exceptionally challenging or even impossible, especially in code injection vulnerabilities.In particular, the high false positive rate in detecting code injection vulnerabilities is primarily caused by us considering the Node.jsfunction require as an unsafe sink.This assumption is not always true.Although an attacker is able to control the imported package name, most times it is not able to also execute an exported function of that package or control the arguments.This is not manifested as an issue in the ground truth datasets, because there were few require functions with dynamic package names.
Ethical disclosure: We performed responsible disclosure for the confirmed vulnerabilities.We contacted all package maintainers, either directly or via Snyk, to explain the discovered vulnerabilities, along with proof of vulnerability.We were unable to find contact information for 3 of them.We provided a 30-day response deadline, and if we do not receive any communication from package maintainers within 30 days we report the vulnerability to CVE (MITRE) [12].At the time of submission, we obtained 3 CVEs: CVE-2023-26156, CVE-2023-49210, and CVE-2023-40582.We received 6 replies, of which 4 developers confirmed the vulnerability: #1 deprecated the package and #2 produced a fix, as a result of our reporting; #3 asked us to create a pull request with a warning, and #4 said that the package was not being maintained anymore, and won't investigate a possible fix.#5 asked for a timeline extension, and #6 did not agree with our assessment.
Takeaway 2: Graph.jsfound 101 exploitable vulnerabilities in the Collected dataset, where 49 of them were not previously reported and were not an intended functionality of the package.

RQ3: Performance Evaluation
Execution time: We measure the time taken by Graph.js and ODGen to detect vulnerabilities in each npm package from both our reference datasets, consisting of 160 packages from VulcaN and Fig. 7. CDF of total time to finish analysis.384 from SecBench.Figure 7 plots the cumulative distribution function (CDF) of the percentage of packages that each tool managed to analyze according to the analysis time of each package.We depict the first 60 seconds, although each package can take as much as 5 minutes to be processed as per our pre-defined analysis timeout.We can see that Graph.jscompletes the analysis for almost all the 544 packages from both datasets.Within just 10 seconds, Graph.js had already finished analyzing more than 95% of the packages.The remaining long tail represents only 10 packages that Graph.js was unable to analyze within the 5-minute limit, accounting for 1.8% of the total.In stark contrast, ODGen showcases limited scalability, successfully analyzing only 71.5% of the packages.Interestingly, in the initial five seconds, ODGen outperforms Graph.js by analyzing packages considerably faster.For instance, by the 2-second mark, ODGen had already analyzed 39.5% of the packages, while Graph.jshad completed only 1.1% of the total.To understand the reasons behind this difference, we calculated the average package analysis times of Graph.js and ODGen for packages that did not time out, grouped by vulnerability type.We further break down the time into two phases: graph construction and graph traversals.Table 6 summarizes these findings.In the table, the columns "Graph" and "Traversals" indicate the average time each tool takes to build its graph and execute the corresponding query."Total" represents the total time.
Considering all vulnerability types, Graph.jsanalyzes a package on average 0.8 seconds faster than ODGen.However, a more detailed analysis by vulnerability type reveals some interesting insights.On the one hand, ODGen's graph traversal is considerably more efficient for most taintstyle vulnerabilities (CWE-22, CWE-78, and CWE-94), with Graph.jspotentially taking up to 4.8 times longer to process a package.This efficiency in ODGen can be attributed to its queries being natively implemented in Python as part of the tool, whereas Graph.jsrelies on Neo4j's query engine, which is slower.Consequently, taint-style detections tend to complete more quickly in ODGen than in Graph.js,explaining the performance discrepancy highlighted in the CDF.Notably, for prototype pollution (CWE-1321), the situation is reversed: ODGen takes significantly longer to analyze prototype pollution vulnerabilities.This delay is primarily due to the considerable expansion in the size of its ODG caused by patterns associated with prototype pollution vulnerabilities [36].
Takeaway 3: Graph.js'saverage package analysis time is 4.61 seconds, and it successfully analyzes 98.2% of all packages from our reference datasets, demonstrating greater scalability than ODGen.

Graph complexity:
We assess the complexity of MDG, by measuring the number of nodes and edges for all analyzed packages, and compare it with ODGen.Table 7 presents the graph size of Graph.js and ODGen for the reference datasets combined, grouped by the number of lines (LoC) of the analyzed package.To ensure a fair comparison with ODGen, we included the AST and CFG nodes used to generate the final MDG, even though they are not used in the queries.The "#" column represents the total number of packages and the "# Graphs" column represents the number Takeaway 4: In 99% of the cases, Graph.jsgenerates graphs significantly smaller than ODGen.

Case Study
We highlight a case study, sourced from the reference datasets, that showcases a prototype pollution vulnerability in the context of a loop.Figure 8 presents a code snippet adapted from the npm package set-value v3.0.0, which is used to set nested properties on an object using dot notation, and is susceptible to a prototype pollution vulnerability (CVE-2021-23440).The MDG of the prototype pollution vulnerability presented in Figure 8 is illustrated in Figure 9.The edges are numbered according to their creation timestamp.The initial graph contains  1 ,  2 , and  3 , as these are the function parameters.In line 2, object  4 (path) is created with a dynamic property, which depends on  1 (edges 1 and 2 ).In line 3,  4 is extended with property length (edge 3 ).When we execute the first iteration of the loop, variable obj is mapped to  1 ; so, in line 6, we update a dynamic property on  1 with a dynamic value, similarly to the running example (edges 4 , 5 , 6 and 7 ).In line 8, we have two versions of obj:  1 , if the if branch was not executed, and  4 otherwise; so, we only extend  1 with a dynamic property edge ( 8 , 9 ), as  7 already has one, and map obj to { 8 ,  9 }.Now, we execute the second (and last) iteration of the loop.In line 6, we update a dynamic property on { 8 ,  9 } with a dynamic value, as before.Due to our cyclic representation, we generate the same abstract location for the same literal object.So, we add edges 5 , 6 , 7 and 10 when updating  8 , and edges 5 , 6 , 7 and 11 when updating  9 .Finally, similarly to the first iteration, extend  8 and  9 with a dynamic property edge ( 12 and 13 ).We can easily recognize the prototype pollution pattern by identifying the pattern  1

DISCUSSION
Currently, Graph.jssuffers from two types of limitations.One pertains to the inherent limitations of static analysis, as it cannot precisely analyze programs that rely on dynamic features of the language.Graph.jsonly analyzes dynamic function calls that can be resolved statically (e.g., we know statically to which function a given variable/property points to) and does not support dynamic code evaluation with eval and the Function constructor.It handles arrays similarly to objects, evaluating indexes as property names, but it may introduce ambiguities, e.g., when the number of elements cannot be determined statically.Nonetheless, Graph.js is able to analyze packages with unimplemented features but can miss vulnerabilities.Graph.jsoutperforms ODGen due to our design decision of not to keep the full AST and CFG information and not to unfold loops and recursive calls.One implication is that the order of two property reads x.a and x.b that are not separated by updates to x, cannot be distinguished in MDG.However, such reads could be reordered while preserving the program semantics.The evaluation results indicate that the precision we gave up allowed Graph.js to be more performant in finding vulnerabilities that we target.
The precision of Graph.js is bounded by the queries presented in §4, which may not encode all the patterns that match a specific vulnerability type, or detect if proper sanitization was employed.Graph.js'squeries can be expanded to identify other taint-style vulnerabilities, such as SQL injection, without modifying the underlying MDG.For instance, to detect SQL injections, one can supply common sinks like mysql.connection.query.The query can also be extended to not report programspecific sanitization functions, reducing false positives.
The soundness proofs established that the abstract graph over-approximates the concrete graph.We could go one step further and define concrete semantics that uses a regular object graph, as opposed to the multiversion one.Collapsing the multiversion graph to include only the latest version would yield the regular object graph.Using this concrete semantics, we can define concrete attack traces.The soundness proof could then be used to show that if there is a real attack trace on the concrete semantics, then there is a corresponding pattern in the concrete MDG, which in turn, means that the pattern exists in the abstract graph, i.e., our MDG does not miss any vulnerabilities.

RELATED WORK
Program analysis using CPGs: CPGs [61] have been used for detecting security vulnerabilities in web application code for various languages, including PHP [2,46], JavaScript [10,29,36], and WebAssembly [7].Khodayari and Pellegrino [29] explored CPGs for detecting cross-site request forgery vulnerabilities in client-side JavaScript.Li et al. [35] introduced a CPG version for identifying prototype pollution vulnerabilities in Node.js applications using an Object Property Graph (OPG).OPG enhances the original CPG by representing JavaScript objects, including variable names and properties.ODGen [36] evolved from this work, introducing the Object Dependence Graph (ODG) to detect Node.js vulnerabilities using graph queries.However, ODGen's combined CPG-ODG structure is complex and lacks soundness proofs.Our work addresses these limitations by tracking object versions using an MDG to improve the effectiveness and trustworthiness of the analysis.Although MDGs were designed to address JavaScript-specific challenges, their underlying ideas can also be applied to reasoning about mutation in other dynamic languages, such as PHP and Python.
Vulnerability detection tools for Node.jsapplications: Various studies [1,6,53,64] have reported a profusion of security vulnerabilities in npm packages and Node.js applications.To detect them, many existing tools employ dynamic code analysis [9,23,[52][53][54]60], sometimes in combination with symbolic execution Xiao et al. [60].Some approaches focus on specific vulnerabilities, such as prototype pollution [30,35] or code and command injection vulnerabilities [41].Others, also generate exploits [9,27,43].Brito et al. [6] studied several static vulnerability scanners, including ODGen and CodeQL [10], and found that ODGen offers the best trade-off between effectiveness and precision among the evaluated tools.Graph.js, while sharing the goal of detecting vulnerabilities in npm packages with the aforementioned work, explores a distinct analysis technique.
Vulnerability detection for client-side JavaScript code on the browser.Researchers have utilized a variety of code analysis techniques to detect malicious JavaScript code [8,15,16], vulnerabilities in JavaScript code in web applications [29,34,37,38,46,55], and browser extensions [17,50,63].DoubleX [17], leverages an Extension Dependence Graph for enhanced automated detection of vulnerable extensions.Although our focus is predominantly on server-side npm packages, Graph.jscan in principle be adapted to the client side.Such an extension would require handling browser-specific APIs and analysis of event handlers.We leave this exploration for future research.
Static analysis of JavaScript code: Abstract interpretation [11] analyzes a program in an abstract domain rather than the concrete domain in which it operates.Some tools leverage abstract interpretation for analyzing errors in JavaScript code [3,13,26,28,31,42,44,45].Although CPG (and MDG) construction is not abstract interpretation in the classical sense defined by Cousot and Cousot [11], abstractly executing the program to construct the CPG shares similarities with computations in an abstract domain, as both aim to capture higher-level properties of programs.Points-to-analysis for JavaScript [18,25,51] aims to identify potential memory locations that a pointer or reference variable might target.While Graph.js integrates points-to information within its MDG, it includes additional information on the program's behavior to enhance the vulnerability analysis.In particular, instead of computing an abstract representation of the final concrete states, it models the entire program execution with new version edges.
(a) Vulnerable code.(b) Example of a benign use of the package.(c) MDG of the program on the left.(d) Exploit for command injection vulnerability.(e) Exploit for prototype pollution vulnerability.

Fig. 1 .
Fig. 1.A motivating example, with a command injection and prototype pollution vulnerability.
allows for the identification of this pattern, where one can easily identify a Proc.ACM Program.Lang., Vol. 8, No. PLDI, Article 164.Publication date: June 2024.

Fig. 4 .
Fig. 4. Concrete sub-MDG of the motivating example in Figure 1a.in 1 , updating the abstract store and graph accordingly; finally, the rule adds a property edge corresponding to the property being assigned to the new version objects contained in  ′ 1 .The rule [Static Property Update] is similar except that NV is used with  being the last argument.Back to our example, rule [Dynamic Property Update] is applied when analyzing line 5, where  = options[branch_name] :=  url.First, expressions options, branch_name and url, are evaluated to { l5 }, { l3 } and { l4 }, respectively.Then, the rule calls NV *  ( ĝ, ρ, { l5 }, { l3 }), which (1) creates a new version of object l5 , represented by the abstract location l6 , via edge l5 ↦ → V * l6 , and (2) updates the variable options to the new abstract location l6 in the abstract store.Finally, the rule extends l6 with the dynamic property * , via edge l6 ↦ → P( * ) l4 , as url is represented by the abstract location l4 .The[If]  rule evaluates both branches of the if statement and combines the results using the least upper bound operator.Finally, the [While] rule computes the least fixed point of the analysis on the body of the loop.When analyzing a loop while(){} on a state ( ĝ, ρ), we must find the smallest state ( ĝ′ , ρ′ ) such that: ( ĝ, ρ) ⊑ ( ĝ′ , ρ′ ) and A (, ĝ′ , ρ′ ) = ( ĝ′ , ρ′ ).Such fixed point is guaranteed to exist because the set of abstract states forms a finite lattice and the analysis is monotone: for any ĝ1 and ĝ2 , ρ1 and ρ2 , and statement , it holds that: ( ĝ1 , ρ1 ) ⊑ ( ĝ2 , ρ2 ) =⇒ A (, ĝ1 , ρ1 ) ⊑ A (, ĝ2 , ρ2 ).

4 .
The property of the first lookup  5 , the property of the sub-object assignment  6 and the value of the assignment  4 are tainted through paths  2 D − → 5 ,  3 D − → 6 , and  4 , respectively.

Fig. 6 .
Fig. 6.Venn diagram of the vulnerabilities detected by Graph.js and ODGen.

6 . 6 D− →𝑜 9 , 𝑜 6 D− →𝑜 7 , 𝑜 7 D−
The property of the lookup  9 , the property of the sub-object assignment  7 and the property of the first lookup  8 are tainted through paths  → 8 , respectively.

•
Line 5:This statement involves a dynamic property update in options[branch_name].In this case, the analysis creates a new version of the updated object.In the example, a new object version for options ( 6 ) is created from its previous version  5 , linked via a version edge  5 Here, the analysis identifies a property update of the static property cmd, which follows the same steps as the dynamic one, except that no dependency edges are created as the name of the property is known at static time.So, the analysis creates a new version of options ( 7 ) linked from its former version with a version edge  6 The last line executes a call to the function exec().First, the analysis must evaluate the lookup expressions options.cmdand options.commit.The first expression trivially evaluates to  8 , since  7 defines the property 'cmd'.For the second lookup, the analysis first constructs node  9 and connects it to the initial version of options,  5 , with a property edge  5

Table 1 .
Base graph traversals.Notation as defined in Section 3.1.Finds sequence of distinct  nodes connecting   to   , via  + 1 distinct edges.If  is not specified, return all distinct paths that start in   .If  = , return   .  }   − →   , where  ≥ 0 Finds a dependency path between node   and node   .If  is not specified, return all distinct paths that start in   .Searches for an object   assignment via dynamic property.Returns   and   .

Table 2 .
Graph Traversals for detecting taint-style and prototype pollution vulnerabilities.

Table 3 .
Summary of the reference datasets per vulnerability type: "Raw Total" show the total number of packages in the dataset; "Total" show the number of packages excluding incorrect annotations and duplicates.

Table 4 .
Effectiveness and precision of Graph.js and ODGen for the VulcaN and SecBench datasets combined.

Table 5 .
Vulnerabilities found by Graph.js in the Collected dataset

Table 6 .
Average time, taken by each analysis phase.

Table 7 .
Graph complexity of Graph.js and ODGen for the VulcaN and SecBench datasets combined.each tool was able to generate before timing out.Note that the total number of packages is different than the number of vulnerabilities presented in previous sections, because one package may contain more than one vulnerability (see §5.1).Similarly to §5.4, we measure the number of edges and nodes of the graph generated by ODGen for each vulnerability type alone.The graphs built by Graph.js are significantly simpler, having on average 7.2× fewer nodes and 2.3× fewer edges than ODGen.MDGs grow linearly with the number of lines of code given that our fixed-point computation algorithm only generates a single node per allocation site, re-using the same node in every iteration.Conversely, ODGen allocates a new node every time an object initializer command is analyzed, leading to the object explosion problem noted by its authors.