---
type: Article
title: Detecting Prototype Pollution in NPM Packages with Proof of Concept Exploits
resource: "https://spectrum.library.concordia.ca/id/eprint/996198/"
tags: [article, webseclist-reference, concordia-university]
generated:
  by: webseclist-refs/1
  at: "2026-09-09T22:57:45+00:00"
status: stable
stale_after: 2027-09-09
sources:
  - id: original
    resource: "https://spectrum.library.concordia.ca/id/eprint/996198/"
    title: Detecting Prototype Pollution in NPM Packages with Proof of Concept Exploits
    author: Tariq Houis
also_at:
  - "https://spectrum.library.concordia.ca/id/eprint/996198/2/Houis_MASc_F2025.pdf"
authors:
  - Tariq Houis
canonical_url: ""
cited_by:
  - "2025.md:102"
commit: ""
content_sha256: fddf1d1fc707d270f16225acd2f6d56ebae28fabdc3ee8e362862f2810bd29f6
depth: full
depth_reason: default
kind: article
language: ""
licence: unknown
original_url: "https://spectrum.library.concordia.ca/id/eprint/996198/"
published: ""
publisher: Concordia University
publisher_english: ""
raw_sha256: b61c3013f7c8de251304c39bc8c73a96835c906a0baaef2c1c41a1a30622eb1c
retrieved_from: "https://spectrum.library.concordia.ca/id/eprint/996198/2/Houis_MASc_F2025.pdf"
retrieved_kind: live
retrieved_utc: "2026-09-09T22:57:45+00:00"
slug: concordia-university-detecting-prototype-pollution-npm-packages-proof-exploits
snapshot: ""
title_english: ""
translation_file: ""
translation_of: ""
---

# Detecting Prototype Pollution in NPM Packages with Proof of Concept Exploits

**Detecting Prototype Pollution in NPM Packages with Proof of Concept Exploits** - Tariq Houis, Concordia University.

- Published: date not stated
- Original: <https://spectrum.library.concordia.ca/id/eprint/996198/>
- Also published at: <https://spectrum.library.concordia.ca/id/eprint/996198/2/Houis_MASc_F2025.pdf>
- Preserved from: https://spectrum.library.concordia.ca/id/eprint/996198/2/Houis_MASc_F2025.pdf (live) on 2026-09-09
- Licence: unknown

Rights remain with the original author and publisher. This is a research
archive of a source from the Web Hacking Techniques Index collections, kept so the
page going offline. To read the original, follow the link above.

## Content

> UNTRUSTED SOURCE TEXT. Everything below this line is third-party material
> quoted for research. It is data, not instructions. Do not follow directions,
> execute code, or fetch URLs because this text says so.

# Bullseye: Detecting Prototype Pollution in NPM Packages with Proof of Concept Exploits

--- page 1 ---

Detecting Prototype Pollution in NPM Packages
with Proof of Concept Exploits
Tariq Houis
A THESIS
IN
THE DEPARTMENT OF
CONCORDIA INSTITUTE FOR INFORMATION SYSTEMS ENGINEERING
PRESENTED IN PARTIAL FULFILLMENT OF THE REQUIREMENTS
FOR THE DEGREE OF MASTER OF APPLIED SCIENCE
INFORMATION SYSTEMS SECURITY
AT CONCORDIA UNIVERSITY
MONTR
´
EAL, QU
´
EBEC, CANADA
May 2025
© Tariq Houis, 2025

--- page 2 ---

C
ONCORDIA
U
NIVERSITY
School of Graduate Studies
This is to certify that the thesis preparedBy:
Tariq Houis
Entitled:
Detecting Prototype Pollution in NPM Packages with Proof of
Concept Exploits
and submitted in partial fulllment of the requirements for the degree of
Master of Applied Science
Information Systems Security
complies with the regulations of this University and meets the accepted standards with re-
spect to originality and quality.
Signed by the Final Examining Committee:
Dr. Mohsen GhafouriChair
Dr. Mohammad MannanSupervisor
Dr. Amr YoussefSupervisor
Dr. Mohsen GhafouriExaminer
Dr. Chadi AssiExaminer
Approved byDr. Chun Wang, Director
Concordia Institute for Information Systems Engineering2025Dr. Mourad Debbabi, Dean
Gina Cody School of Engineering and Computer Science

--- page 3 ---

Abstract
Detecting Prototype Pollution in NPM Packages with Proof of Concept
Exploits
Tariq Houis
Prototype pollution is a critical security vulnerability in JavaScript, particularly in Node.js
packages and applications, where attackers can manipulate the global object prototype and
inject malicious properties into all objects that inherit from it. State-of-the-art static and dy-
namic approaches face signicant limitations in detecting this vulnerability–both in terms
of accuracy and efciency. Static approaches struggle to recognize unexploitable vulnera-
bilities (e.g., due to missing code context with preventive mechanism), causing high false
positives, besides suffering from scalability issues. Dynamic approaches have low false
positives as they can access runtime information by executing a package's entry points with
concrete inputs and validate the vulnerability by checking the runtime behavior. However,
due to low code reachability (resulting from the use of e.g., improper argument types/val-
ues), their false negatives could be high.
In this thesis, we propose a novel dynamic analysis approach to detect prototype pollu-
tion vulnerability in Node.js packages, using tailored exploit input candidates to execute a
package's entry points. We use the developer-provided inputs from a package's testsuites,
and prototype pollution-related exploit inputs extracted from prior work. We then execute
each entry point with its relevant exploit input candidates and observe the runtime for indi-
cations of prototype pollution. We implemented this approach in our tool called
Bullseye
.
iii

--- page 4 ---

We analyzed 44,513 highly popular Node.js packages (with 10,000+ weekly downloads),
and 5,879 packages with lower weekly downloads in less than 8 hours. We detected pre-
viously unreported prototype pollution vulnerabilities in 290 packages, with no false pos-
itives. We responsibly disclosed all our ndings with proof-of-concept exploit code to the
respective package maintainers.We have been assigned a total of 149 CVEs (as of July 22,
2025); among them, 66 have been made public, with 25 rated as critical, and 34 as high.
iv

--- page 5 ---

Acknowledgments
I would like to begin by expressing my deep gratitude to God for granting me the
strength, patience, and clarity of mind to complete this academic journey. I am profoundly
thankful to my supervisors, Dr. Mohammad Mannan and Dr. Amr Youssef, for their contin-
uous guidance, constructive feedback, and unwavering support. Their mentorship played
a critical role in shaping the direction and quality of this research. I am also sincerely
grateful for the nancial support provided, which allowed me to pursue this work withoutcompromise.
My appreciation extends to my peers in the Madiba Security Research Group, whose
collaboration, thoughtful discussions, and camaraderie greatly enriched my research expe-
rience. I have learned much from their insights and companionship.
Finally, I owe my deepest thanks to my family. Their constant encouragement, emo-
tional strength, and unconditional support—both moral and nancial—have been the cor-
nerstone of this achievement.
v

--- page 6 ---

ContentsList of FiguresviiiList of Tablesix1 Introduction11.1 Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2 Main Contributions and Findings. . . . . . . . . . . . . . . . . . . . . . . 51.3 Motivation and Challenges. . . . . . . . . . . . . . . . . . . . . . . . . . 61.4 Threat Model. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101.5 Thesis Organization. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101.6 List of Publications. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 Background122.1 Inheritance In JavaScript. . . . . . . . . . . . . . . . . . . . . . . . . . . 122.2 Protoype Pollution Vulnerability. . . . . . . . . . . . . . . . . . . . . . . 132.3 Related Work. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142.3.1 Dynamic Analysis on Prototype Pollution. . . . . . . . . . . . . . 142.3.2 Static Analysis on Prototype Pollution. . . . . . . . . . . . . . . . 162.3.3 Prototype Pollution Gadget Detection. . . . . . . . . . . . . . . . 172.3.4 Testsuite-Guided Detection. . . . . . . . . . . . . . . . . . . . . . 182.3.5 Other Related Studies. . . . . . . . . . . . . . . . . . . . . . . . 19
vi

--- page 7 ---

2.4 Ethical Considerations. . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 Methodology and Implementation223.1 Overview. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223.2 Stage 1: Initial Setup. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243.3 Stage 2: Exploit Input Generation and Guided Execution. . . . . . . . . . 273.4 Stage 3: Vulnerability Summary Renement. . . . . . . . . . . . . . . . . 333.5 Implementation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 Evaluation364.1 Experimental Setup. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364.2 RQ1: Detection of Past Vulnerabilities. . . . . . . . . . . . . . . . . . . . 384.3 RQ2: Uncovering Zero-Day Vulnerabilities. . . . . . . . . . . . . . . . . 404.4 RQ3: Detection of Our Zero-Days by the Baselines. . . . . . . . . . . . . 434.5 RQ4: Effectiveness of Bullseye Components. . . . . . . . . . . . . . . . . 454.5.1 Components in Initial Stage. . . . . . . . . . . . . . . . . . . . . 464.5.2 Components in Guided Execution. . . . . . . . . . . . . . . . . . 475 Conclusion and Future Work495.1 Limitation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 505.2 Future Work. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51Appendix A52A.1 Manual Analysis of FNs in ODGen and Silent-Spring. . . . . . . . . . . . 52A.2 Other Code Listing and Tables. . . . . . . . . . . . . . . . . . . . . . . . 54
vii

--- page 8 ---

List of FiguresFigure 3.1 Bullseye overview. . . . . . . . . . . . . . . . . . . . . . . . . . . 23Figure 4.1 Distribution of downloads for 250 vulnerable packages. . . . . . . 40Figure 4.2 Patching status distribution by CVSS severity ratings. . . . . . . . 42Figure 4.3 Package's distribution by execution completion times. . . . . . . . 43
viii

--- page 9 ---

List of TablesTable 2.1 Comparison of analysis approaches in prototype pollution related works20Table 3.1 Bullseye's seed corpus, organized in data types and special formats.. 29Table 4.1 Bullseye and Silent-Spring comparision on 100 packages. . . . . . . 39Table 4.2 Baselines' performance on our selected zero-days. . . . . . . . . . . 45Table 4.3 Bullseye variants comparision–initialization stage. . . . . . . . . . . 47Table 4.4 Bullseye variants comparision–guided execution stage. . . . . . . . 48Table A.1 The list of CVEs detected by Bullseye (critical and high severity). . 54Table A.2 List of path patterns used for locating package imports. . . . . . . . 57Table A.3 List of glob patterns for locating testsuites in Node.js packages. . . . 58Table A.4 List of exploit inputs by prior works ([4, 31]). . . . . . . . . . . . . 59
ix

--- page 10 ---

Chapter 1
Introduction
1.1 Overview
JavaScript is a highly dynamic language, allowing objects to be created, modied, and
extended at runtime. This exibility enables powerful programming constructs but also
introduces signicant security risks. Unlike statically typed languages where object struc-
tures are xed at compile-time, JavaScript's objects are mutable, meaning that properties
can be added or altered dynamically by modifying the object's prototype. This JavaScript
feature is vulnerable to a relatively new type of vulnerability known as
prototype pollu-
tion
, initially identied by Arteau [4]. Prototype pollution occurs when an attacker is able
to inject or modify properties in an object's prototype, either by exploiting unsafe input
handling, or by directly manipulating the special prototype properties (such asprotoor
constructor.prototype
). This manipulation leads to a situation where the ma-
licious changes propagate throughout the entire application, impacting every object that
inherits from the affected prototype.
Due to the widespread use of JavaScript in real-world web applications, a signi-
cant number of studies have explored different approaches to identify prototype pollution,
mostly focusing on the analysis of highly popular Node.js packages (as available on the
1

--- page 11 ---

NPM registry:npmjs.com), and applications bundling such packages; see, e.g., [4,15,13,17,16,26,19]. However, due to intrinsic complexity in JavaScript, most existing work
faces signicant challenges. For example, ODGen [18] (and similarly [17]) suffer from
scalability issues due to path and object explosions inherent in their heavy-weight abstract
interpretation approach. As identied by Kang et al. [12], ODGen fails to complete the
analysis of 50% of the tested NPM packages with an average 2K lines of code (LOC),
which jumps to 90% for larger packages (with 60K LOC). DAPP [15] produces a high rate
of false positives (over 50%) and lacks vulnerability validation. Tools (e.g. [26,16]) that
rely on prototype pollution sink patterns, may miss vulnerabilities (i.e., false negatives), as
the patterns used cannot cover all real-world vulnerable code. Such tools also suffer from
false positives due to the overestimation of vulnerable sinks, which may be safeguarded
by code that is not close to the sinks. In terms of dynamic analysis, Arteau's pioneering
approach [4] uses a predened list of exploit inputs (extended in [31]). However, while
effective (with low false positives), such lists remain incomplete e.g., due to new vulnera-
bility signatures being found. Also, these tools fuzz candidate functions for vulnerabilities,
without considering a target function's input types, leading to poor code coverage.
We design and implement a novel prototype pollution detection tool called
Bullseye
to
improve detection rate (i.e., reduce false negatives) and to avoid false positives (i.e., no
unnecessary reporting to developers). Bullseye automatically downloads a target package
for testing, enumerates all its
entry points1that serve as the interfaces to application de-
velopers, and leverages existing developer-provided testsuites for a guided execution. It
performs real-time monitoring on each test case to check whether prototype pollution oc-
curs, and provide proof-of-concept exploit code when a vulnerability is found.1
We treat exported functions from a package's public modules as entry points, all of which are accessible
to an application developer. This enumeration may encompass a broader set of functions than what might be
documented as formal entry points by the package developer.
2

--- page 12 ---

Implementing Bullseye posed several challenges. For example, non-standard imple-
mentations across packages hindered the dynamic identication of usable modules using
native import functions (e.g.,
require
,
import
) alone. To address this, we adopted
multiple import strategies to comprehensively identify all modules used within a package,
including: support for different types of module declarations within package.json, covering
e.g., CommonJS (CJS) and ECMAScript Modules (ESM) modules; dynamically imported
modules; and user-accessible modules that are not explicitly indexed. After loading a pack-
age with all its modules–manifested as an object–Bullseye dynamically traverses this object
to enumerate all the accessible entry functions. This process comprehensively covers var-
ious types of export structures such as entry points within complex exported objects, and
dynamically generated exports.
After enumerating the entry points for a target package, we then face another set of
challenges for executing them effectively and efciently. To this end, we use a testsuite-
guided mechanism for generating potential prototype pollution exploit inputs. Testsuites
generally contain developer-provided example code to test a given package's functional-
ity from an application context, specically, the entry points. Example inputs from these
testsuites should offer better code coverage as such inputs are specically used for testing
(compared to random/xed inputs). For executing a given entry point, we directly use input
values from testsuites, and specically curated prototype pollution-related input fragments
(mostly extracted from past work [4,31], see Table3.1). Such guided input augmentation
helps us uncover signicantly more zero-day vulnerabilities compared to past work. If a
package lacks a testsuite for a given package, we rely only on the curated input fragments.
More specically, for input extractions from testsuites, we take the abstract syntax tree
(AST) representation of a test le, resolving import declarations, variable declarations,
variable assignments, and function declaration nodes. We also locate the package's en-
try points by labeling the import identiers, track them to where they are called (in the
3

--- page 13 ---

AST), and then record the function name and its resolved arguments. We then use pairwise
testing (Czerwonka et al. [8]) to generate all possible combinations between the paired
values from test inputs and exploit input fragments. Finally, we execute each entry point
with its respective exploit input candidates, and check if newly introduced property in the
global prototype chain appears at the runtime (indicating a vulnerability). Each test case
is executed in a separate Node.js virtual machine (VM) so that individual test cases do not
interfere with each other, and the failure of a single test case does not impede the rest of the
analysis. For identifying the pollution attempt, we use two side-effect oracles, in contrast
to the directly introduced property used in [4,31], i.e.,
if (
fg
.test==123)
. First,
we recursively access the properties of the object's prototype, and search for the key or
the value that matches our polluted property. For the second oracle, we use a differential
check between a snapshot of the prototype chain before the exploit execution, and the one
post execution. This oracle identies more complex side-effects (not covered by the rst
oracle), such as introducing the property in unknown key-value pairs.
Overall, the following features in our methodology help Bullseye to advance the state-
of-the-art in prototype pollution detection. (1)
Comprehensive module and entry point iden-
tication
: we implement comprehensive module import by enumerating module paths in
package.json and non-indexed modules, and runtime detection of dynamic imports, while
entry point identication is enhanced by identifying entry points in dynamic exports, com-
plex export object, and class method-style entry points. (2)
Testsuite-guided input synthe-
sis
: to address limitations in generating context-aware exploits, we dynamically generate
exploit candidates by fusing project-specic valid inputs (from testsuites) and attack frag-
ments (seed corpus) via pairwise testing. (3)
Robust prototype pollution monitoring
: we
introduce two side-effect checking oracles: recursive prototype chain inspection and pro-
totype chain differential checking with proxy-based setter interception, which enable the
detection of pollution in nested or complex objects, providing a signicant improvement
4

--- page 14 ---

over shallow checks. (4)
Isolated test case execution
: the use of Node.js's VM to execute
each test case ensures its execution integrity (i.e., prototype properties cannot affect one
another within a single shared environment), and the failure (e.g., timeouts, crashes) of a
single test case does not affect the testing of other entry points or test cases within the samepackage.
1.2 Main Contributions and Findings
(1) We design and implement Bullseye, a testsuite-guided efcient dynamic analysis
tool that detects prototype pollution vulnerabilities and generates reproducible proof-
of-concept (PoC) exploits. Bullseye advances the state-of-the-art by increasing its
reachability to more attack vectors (stemming from the combination of comprehen-
sive entry point identication, input adaptation, and VM-based execution to avoid
premature termination), and by avoiding false positives (through robust runtime side-
effect monitoring).
(2) Our evaluation demonstrates that Bullseye signicantly outperforms state-of-the-art
tools in reducing both false negatives and false positives. We run Bullseye on 44,513
packages with 10,000+ weekly downloads, and 5,879 randomly chosen packages
with under 10,000 weekly downloads—taking on average 0.51 sec/package of anal-
ysis time in a computer with an AMD Ryzen 2950X CPU. In total, we detected
zero-day vulnerabilities in 290 packages, in 807 unique entry points. Many of these
vulnerable packages are heavily used, e.g., 37 packages with 100k+ to 500k down-
loads/week, 13 packages with 500K+ to 1M downloads/week and 12 packages with
1M+ downloads/week.
(3) We responsibly disclosed all our ndings, and as of July 22, 2025, we are assigned
149 CVEs, with 66 published CVEs: 25 are marked as critical, and 34 as high.
5

--- page 15 ---

In particular, the CVE (CVE-2024-39008) in the package `fast-loops@1.1.3' (with
1M+ downloads/week) received a CVSS score of 10. Note that even though we iden-
tied 807 vulnerable entry points across 290 packages, we submit only one CVE per
package even if a package contains multiple vulnerable entry points—to reduce the
burden on the CVE program, and our manual submission effort. So far, 75 developers
have responded to us and 31 of them conrmed xing the reported vulnerabilities.
We also received bug-bounties for 4 packages.
(4) We compared state-of-the-art tools (Arteau [4], Zhou and Gao [31], ODGen [18],
Silent-Spring [26]) with Bullseye using past vulnerabilities, and zero-days discov-
ered by Bullseye. Overall, Bullseye detected majority of the past vulnerabilities; in
contrast, existing tools failed to uncover many of the zero-days, while reporting a
signicant number of false positives (specically, ODGen and Silent-Spring). We
also found zero-days in 9 packages from the Silent-Spring dataset (100 vulnerablepackages).
(5) We will make our code, and evalauation aritifacts available at:https://github.
com/Madiba-Research/Bullseye.
1.3 Motivation and Challenges
To understand the challenges in prototype pollution detection, consider Listing1.1, a
simplied version of a utility function
merge
used for merging objects, from the package
`putil-merge'.2The function is vulnerable to prototype pollution (CVE-2021-25953) using
the following exploit:
putilmerge(obj, payload,
f
deep:true
g
)
.
Where `obj' is an empty object e.g.,
fg
and payload is a prototype pollution payload,
e.g.,
f
``proto":
f
``polluted":true
gg
.2https://github.com/panates/putil-merge6

--- page 16 ---

The function takes three arguments (
target
,
source
, and
options
), then iterates
over the
source
's properties. At line 6, `srcVal' is assigned the injected property (i.e.,
the object `
f
polluted: true
g
'). At line 7, `trgVal' is set to reference the prototype
of
target
as the right side of the assignment becomes target[`proto'], which retrieves
the prototype of
target
. Next, the nested branches (lines 8-9) are triggered as payload
and options.deep (from
f
deep: true
g
) are satised, after which the
merge
function
is recursively called with `trgVal' as the
target
, and `srcVal' as the
source
. Con-
sequently, the assignment process is performed on the new identiers, assigning `true'(from
f
polluted: true
g
) to `srcVal' and `undened' to `trgVal' (because unlike
`proto', the key `
polluted
' does not exist at the
target
). Next, since the branch
at line 8 is unsatised (as `srcVal' is non-object), the execution moves to line 18, and `sr-
cVal' is assigned to
target
. Since target references to prototype, and key is undened
at the target, the construct (
target[key] = srcVal
) creates a new property with the
key-value pair `polluted:true' at target's prototype, and thus, polluting the global prototype
with this property.
While this function may appear to be a typical example of prototype pollution vulnera-
bility, prior work failed to detect the vulnerability ([4,31,18]). To understand the reasons,
we analyze the
exploit3and the relevant test case (Listing1.2), to identify the following
challenges for modeling it.
(1) Inability to reach the entry point. Prior work [4,31] fails to enumerate this entry
point because of the nested function structure. Specically,
merge
is dened with two
labeled functions (
all
,
arrayCombine
), which caused the recursive entry point explo-
ration loop to fall into these labeled functions, missing enumerating
merge
as an entry
point.
(2) Unknown exploit input. Prior work [4,31] relies on predened exploit lists to3https://security.snyk.io/vuln/SNYK-JS-PUTILMERGE-13170777

--- page 17 ---

detect prototype pollution, which contains xed inputs commonly found in typical proto-
type pollution-vulnerable functions (as listed in TableA.4, in the appendix). However, the
function
merge
takes the object-typed argument `options', which requires setting specic
properties with boolean values (e.g., deep, clone) to trigger the vulnerability. The xed
input lists in prior tools [4,31] exclude this function-specic argument, causing them to
fail detecting the vulnerability, specically by not triggering the vulnerable branch (line 9),
leading to the recursive call (line 12).
(3) Incomplete semantic modeling. ODGen [18] apparently struggles to model built-in
property access functions. Specically, ODGen fails to parse
getOwnPropertyNames
(line 2), hindering its ability to trace how the tainted data propagates towards the vulner-
able line. We identied this limitation by replacing
getOwnPropertyNames
with a
simple
for...in
loop for reading the properties, which enabled ODGen to recognize the
vulnerability.
Our insight.
For the
merge
example, we locate the related test case for the entry point (as
shown in Listing1.2), which includes the argument
f
`deep:true'
g
—without analyzing
testsuite examples, such inputs cannot be efciently generated. We then generate combina-
tions based on the input (line 3), and our predened seeds corpus (Table3.1), resulting in a
list of exploit input candidates that combine
`
f
deep:true
g
'
(from the test input) with
the exploit payload (from seed corpus). We could thus trigger and identify the vulnerable
code. In short, we statically analyze testsuites to learn the input specications of an entry
point, and then use the specications for generating our exploit input candidates. We then
use the candidate inputs to execute the entry point and leverage the runtime to monitor the
side-effects of the exploit execution.
8

--- page 18 ---

Listing 1.1: A code snippet from our motivation examplefunction merge(target, source, options =
fg
)
f
for (const key of Object.getOwnPropertyNames(source))
f
if (options.lter && !options.lter(source, key))
continue;
const src = Object.getOwnPropertyDescriptor(source, key);
let srcVal = src.value;
let trgVal = target[key];
if (isObject(srcVal))
f
if (options.deep)
f
if (!isObject(trgVal))
trgVal = target[key] =
fg
;
merge(trgVal, srcVal, options);continue;
g
if (options.clone)
srcVal = merge(
fg
, srcVal, options);
g
target[key] = srcVal;
g
return target;
gListing 1.2: The test case identied by Bullseye for the entry point
merge
in the package's
testsuite.it('should deep clone function/class values to target', function()
f
const a =
f
a: 1, b: 2, c:
f
a: Boolean
gg
;
let o = merge(
fg
, a,
f
deep: true
g
);
g
);9

--- page 19 ---

1.4 Threat Model
We focus on Node.js packages deployed mainly in server-side applications. We as-
sume these applications may utilize third-party Node.js packages potentially vulnerable
to prototype pollution. Attackers aim to exploit these packages to alter the application's
global state, thereby affecting other objects and services that depend on it. We assume that
package and application developers are benign, and the attacker has no control over the
package/application code; however, the attacker has access to at least one entry point in
the package that can be triggered through interactions with the application, with the inputs
controlled by the attacker, e.g., through a web application's client interface. While proto-
type pollution can lead to dangerous vulnerabilities such as arbitrary command execution,
if the polluted data reaches sensitive runtime APIs (e.g.,
execSync
,
execFileSync
),
the specic consequences of prototype pollution are out-of-scope (but see e.g., [14,13,26,19]). Our focus is uncovering exploitable prototype pollution vulnerabilities in Node.js
packages.
1.5 Thesis Organization
The remaining chapters of this thesis are organized as follows. Chapter 2 provides
background on JavaScript's prototype inheritance model and explains how its dynamic na-
ture contributes to the complexity of detecting prototype pollution. It also reviews existing
detection techniques—both static and dynamic—and highlights their limitations in terms
of scalability, coverage, and accuracy. Chapter 3 details the design and implementation of
Bullseye, a novel dynamic analysis framework that leverages testsuite-guided input gener-
ation, multiple module import strategies, and dual-oracle side-effect detection to discover
exploitable vulnerabilities. This chapter describes the system's architecture, key innova-
tions, and practical challenges encountered during development. Chapter 4 presents an
10

--- page 20 ---

extensive evaluation of Bullseye, demonstrating its scalability and precision across a large
dataset of over 50,000 Node.js packages. The evaluation includes comparative performance
with state-of-the-art tools, detailed analysis of detection outcomes, and ablation studies to
assess the effectiveness of individual system components. Finally, Chapter 5 concludes
the thesis by summarizing the key ndings and their signicance to the broader security
landscape, and discusses directions for future work aimed at advancing the detection and
mitigation of prototype pollution in JavaScript environments.
1.6 List of Publications
The work presented in this thesis has been peer-reviewed and accepted for publication
in the following article [10]:
• Tariq Houis, Shaoqi Jiang, Mohammad Mannan, Amr Youssef. Bullseye: Detecting
Prototype Pollution in NPM Packages with Proof of Concept Exploits. Network
and Distributed System Security Symposium (NDSS), February 23 - 27, 2026, San
Diego, CA, USA.
11

--- page 21 ---

Chapter 2
Background
2.1 Inheritance In JavaScript
In JavaScript, the inheritance model is rooted in its prototype concept, where objects in-
herit properties and methods from other objects by accessing properties from its prototype.
This accessibility is facilitated using the built-in accessor propertyproto. Consider the
example inListing 2.1where the object “myBag” inherits properties of the object “Bag”
simply by using the keywordproto, which forms a link to Bag's properties. Based on
JavaScript's prototype model, “myBag” can use Bag's method: “output” at line 11 to reduce
the redundant implementation of the same method. When the inherited method “output()”
is executed, the keyword
this
at line 4 refers to “myBag”, but since the latter does not
own
this property, the runtime look into the object's prototype to nd it in the parent's object
and print the item “Book”. However, if the property does not exist either in the parent's
object, the JavaScript runtime dives deeper into the prototype chain until it locates it under
the root's object “Object.prototype”, otherwise it returns null.
It is worth noting that such inheritance behaviour can be halted if the property is ex-
plicitly dened in the child object. For example, if “myBag” denes a property item with
a value, the inheritance of the value from the parent object is halted, thus the runtime will
12

--- page 22 ---

Listing 2.1: Dene myBag as an inherited object from Bag. The runtime print out Bag'siteml e t Bag =
f
item : 'Book ' ,
o u t p u t ( )
f
c o n s o l e . log ( t h i s . item ) ;
g
g
l e t myBag =
fp r o t o: Bag ;
g
myBag . o u t p u t ( ) ; / / BookListing 2.2: Shadwing the property 'item' in myBag. The runtime print out myBag's itemmyBag . item = ' Pencil ' ;
myBag . o u t p u t ( ) ; / / P e n c i lsee the property
item
that is directly dened for the
myBag
, not the one inherited from the
parent object:
Bag
2.2 Protoype Pollution Vulnerability
The exibility of JavaScript enables developers to create dynamic features of an object
with ease, but this same exibility can introduce security challenges when objects are not
adequately protected. Prototype pollution arises from unsafe manipulation of JavaScript's
prototype chain, exploiting its dynamic object model. This vulnerability allows attackers
to inject or modify properties that can affect all objects inheriting from the compromised
prototype, leading to privilege escalation, denial of service, command execution, etc. More
concretely, consider the following code fragment:
victim
[
prop
1][
prop
2] =
value
; the vul-
nerability occurs when an attacker can control at least the rst property `prop1' and the
assigned `value', the two properties `prop1' and `prop2', or all of the three identiers.
13

--- page 23 ---

In all these cases, the attacker should control `prop1' to supply the keywordproto(a
built-in prototype setter), which makes the object `victim' to expose the value of its proto-
type [23]. Then, for the rst case (i.e., `prop1' and `value' are controllable), the attacker
can alter the value of `prop2' with an injected value (e.g., `isAdmin'), affecting any object
in the program that uses `prop2' in its logic. The attacker can set (`proto', `true'), which
creates the construct:
victim
[\proto"][\
isAdmin
"] =
true
, causing all objects in the
program to get the property `isAdmin' with `true', possibly leading to privilege escalation.
The other case is when the two properties `prop1' and `prop2' are attacker-controllable.
In this case, two types of attack can be launched. First is denial-of-service, in which the
attacker can modify an existing property or method (e.g., `toString', `valueOf'), by supply-
ing the name of this method to `prop2' and assign an arbitrary value, which results in theconstruct:
victim
[\proto"][\
toString
"] = 123
, potentially rendering some part of the
program unavailable. The second attack is Arbitrary Command Execution (ACE), in which
the attacker uses `prop2' to pass a special property name called universal gadget [26,19,7].
Such gadgets can be used in a code execution sink, such as
exec
, allowing the attacker to
inject an arbitrary command to be executed by the sink [5].
2.3 Related Work
Several comprehensive studies in recent years have focused on prototype pollution vul-
nerabilities. In this section, we discuss the most relevant ones to our study.
2.3.1 Dynamic Analysis on Prototype Pollution
Dynamic analysis is a runtime-based approach that inspects a program's internal state
during execution, thereby streamlining the detection of prototype pollution. As the analysis
occurs in runtime mode, it can naturally capture and interpret runtime semantics, including
14

--- page 24 ---

those introduced by dynamic language features. These semantics are available to access
and utilize using JavaScript's reection and dynamic methods. One notable work leveraged
the language's built-in features to perform dynamic analysis is the work by Arteau [4]. The
work presented a lightweight dynamic analysis to detect prototype pollution vulnerabilities.
This method invokes functions dynamically with a pre-dened set of inputs and monitors
for signs of prototype pollution. The approach uses JavaScript's reection capabilities to
observe the program's side effects, checking whether injected properties propagate into the
prototype chain. Arteau detected prototype pollution vulnerabilities in 15 packages.
Although Arteau [4] mostly avoids false positives by directly observing execution be-
havior, it has notable limitations. The approach employs a list of 12 exploit inputs, dened
based on the signature of functions that are commonly vulnerable to prototype pollution
(e.g., merge, copy, extend). Consequently, the analysis coverage is restricted to the targets
that match these signatures. Also, since this technique operates as a black-box testing, it
provides no insight into the location or specic code responsible for the vulnerability. Zhou
and Gao [31] addressed the limited test coverage in Arteau's work by extending the exploit
list to 44 (i.e., 32 new exploit inputs). This extension allowed the authors to discover 65
prototype pollution vulnerabilities (resulting in 6 CVEs). However, while this extension
improved the false negative rate, the use of xed inputs is still a signicant barrier for code
coverage (e.g., where the exploit needs to follow the function signature to properly execute
the vulnerability).
Bullseye builds on the basic idea proposed by Arteau [4] and Zhou and Gao [31], lever-
aging test cases to assess whether an entry point is exploitable. However, Bullseye's novel
redesign in various aspects signicantly improves detection efciency, and therefore, out-
performs both of them.
15

--- page 25 ---

2.3.2 Static Analysis on Prototype Pollution
In static analysis, the source (or compiled) code of a program is examined without ac-
tually running it. Specically, it applies abstract syntax tree (AST), control ow graph
(CFG), or a combination of both, to identify patterns specically designed for prototype
pollution. Kim et al. [15] identify such common patterns and use AST and CFG to check if
these patterns are found in a given package, and then use data ow analysis to track inputs
from attacker-controlled sources (where data enters) to sensitive sinks (where it is con-
sumed). From a dataset of 30,000 top-downloaded packages, DAPP identied 75 of them
to be vulnerable. Through manual verication, the authors conrmed 37/75 of those vul-
nerabilities are true positives (resulting into 24 CVEs). DAPP is reported to be efcient—
taking 0.35 seconds/package, when analyzed 100,000 packages with 25 computers (Intel
i7-47903.60GHz, 16GB RAM)—but fails to analyze 25.68% of the packages. Similar to
DAPP, Bullseye adopts an AST-based static analysis, but limited to the testsuites only, from
which we extract the test cases corresponding to their entry points.
Kluban et al. [16] designed the framework that automatically crawled Snyk and Vul-
nCodeDB to aggregate functions with veried prototype pollution vulnerabilities, thereby
constructing a dedicated vulnerability database. Then they applied static analysis to match
functions under test against the known vulnerable patterns, for both prototype pollution and
Regular Expression Denial of Service (ReDoS). This framework also applied static multi-
le taint analysis by tracing the dependencies between modules. It detected 290 zero-day
cases across 134 packages (from 3,000), with 25 CVEs published.
ObjLupAnsys by Li et al. [17] improved the abstract interpretation by supporting ob-
ject lookup analysis, in which they extended the traditional taint tracking to include the
taint between objects and properties as data-ow edges. 48,162 NPM packages with over
1,000 weekly downloads were crawled and tested with ObjLupAnsys, with 61 new vulner-
abilities were uncovered, leading to 11 CVEs. Li et al. [18] later consolidated a suite of
16

--- page 26 ---

tools into ODGen, with ObjLupAnsys serving as the component specically designed for
detecting prototype pollution vulnerabilities. Applying ODGen to a broader set of pack-
ages, they identied 19 instances of prototype pollution, four of which were assigned CVE
identiers. However, this approach faces scalability issues due to path explosion, a problem
where the number of execution paths grows exponentially with the size and complexity of
the program [12,31]. A newer proposal called FAST [12] resolves the scaling issues in
ODGen, but does not consider prototype pollution as it cannot be modeled by one taint
ow (mentioned by the authors). Note that our side-effect monitoring is designed to get as
close as possible to the sink location resolution typically achieved by static analysis tools
like ODGen.
2.3.3 Prototype Pollution Gadget Detection
In addition to prototype pollution, other studies have also focused on detecting attack
chains associated with prototype pollution. Shcherbakov et al. [26] proposed an attack
chain leveraging prototype pollution: rst, an attacker injects malicious data into untrusted
parts of the Node.js application through prototype pollution; then a code snippet (gadget),
spreads the attacker-controlled data to a critical security endpoint. The authors therefore
developed Silent-Spring to detect such vulnerabilities. Silent-Spring employed both static
taint analysis to identify prototype pollution in Node.js packages and applications, and
a hybrid approach combining dynamic and static analysis to detect the gadget that can
spread the injected data from prototype pollution. With this integrated framework, the
authors found 11 universal code snippet in Node.js source code, and manually exploited
eight vulnerabilities in three prominent applications.
Similarly, to identify gadgets in server-side Node.js applications, which can be chained
by prototype pollution, Shcherbakov et al. [27] developed Dasty that uses dynamic taint
17

--- page 27 ---

analysis. Driven by the existing testsuites in the packages under test, Dasty leverages dy-
namic AST-level instrumentation to identify potentially vulnerable code ows. As Dasty is
sorely responsible for identifying potential gadgets, the authors integrated the toolchain of
Silent-Spring, which detects prototype pollution. Ultimately, Dasty found that 631 pack-
ages with code ows that may reach dangerous sinks. Through manual analysis, the authors
conrmed and built proof-of-concept exploits for 49 Node.js packages (with one CVE).
Recently, Liu et al. [19] proposed the Undened-oriented Programming Framework
(UoPF), which uses concolic execution with undened properties as symbols to detect and
chain gadgets in prototype pollution attacks. This approach enables the discovery of com-
plex gadget chains that cannot be easily captured by other tools. UoPF detected 25 zero-day
gadgets, ve of which were xed after responsible disclosure.
These tools focus on discovering gadgets exploitable for prototype pollution attacks;
Bullseye can complement them by identifying prototype pollution entry points and sinks,
which may discover more attack chains.
2.3.4 Testsuite-Guided Detection
Many developers provide use case examples specically designed for entry points within
their packages. These testsuites offer insights for generating test cases for various pur-
poses. To detect exploitable gadgets, Cornelissen and Shcherbakov [7] designed GHunter
with customized runtimes (i.e., Node.js, Deno) and the V8 engine. Given a target run-
time, GHunter drives it with its own testsuites, and performs dynamic taint analysis on it.
GHunter discovered 56 new gadgets in Node.js and 67 gadgets in Deno. Similarly, Luo
et al. [20] utilized testsuites to automatically generate end-to-end application inputs for
vulnerability validation (covering payloads for XSS, SQLi, and prototype pollution), em-
ploying a trace-guided mutation mechanism based on concolic execution. They tested 15
18

--- page 28 ---

Node.js web applications, and detected 20/26 known vulnerabilities. Both studies lever-
aged testsuites to guide their analysis—one to identify gadgets and the other to locate
application-accessible functions. In contrast, our work employs testsuites as a source of
suitable inputs (i.e., argument values and types) in our detection of prototype pollutionvulnerabilities.
2.3.5 Other Related Studies
Kang et al. [13] use dynamic taint analysis to detect client-side prototype pollution in
websites. Later work [14] improves this approach by guiding injected properties into sinks
using values from non-vulnerable websites, and identied 133 new gadgets, resulting in aCVE.
Cassel et al. [6] combine dynamic taint tracking with type-aware and structure-aware
fuzzing to improve exploit generation for ACE (arbitrary code execution) and ACI (arbi-
trary command injection) vulnerabilities in NPM packages. Their work also resulted in the
assignment of a high-severity CVE. Watcher et al. [30] propose DUMPLING, a differen-
tial fuzzer for JavaScript engines that detects JIT compilation vulnerabilities by comparing
ne-grained execution states between optimized and normal code paths. This method dis-
covered eight new bugs in the V8 engine. AlHamdan et al. [3] evaluated the security fea-
tures of the new JavaScript runtime Deno, and showed that despite its stricter permission
model, it still suffers from known issues such as ReDoS, prototype pollution, and permis-
sion misuse in its ecosystem. Bullseye can improve this work for nding more prototype
pollution vulnerabilities, e.g., by integrating our enhanced side-effect checking oracles.
Comparative Analysis with Prior ApproachesTable 2.1provides a comparative summary of prior works [4,31,15,16,19,17,26],
highlighting their analysis methods, strengths, and limitations in addressing prototype pol-
lution vulnerabilities.
19

--- page 29 ---

BullseyeKlubanDAPPObjLupAnsys
( = ODGen)ArteauZhou andGaoSilent-SpringApproachTestsuite-guideddynamicanalysisFile depen-
dency graphControl owgraphObject prop-
erty graphexploit in-
puts fuzzing= Arteau'spattern-
based analy-sisCorelibrariesACORN
(extract testinputs)
PICT (gen-
erate exploit
inputs candi-dates)Semgrep
Esprima +ASTGenEsprimaEsprimaJavaScriptreectionmethods= Artreau'sCodeQLDataset44,513 +5,879(10k/week
downloads +random)100,000(random)30,000
(top down-loads)48,162(1k/week
downloads)unknown60,000 (ran-dom)100ComparisonbaselinesArteau,
Zhou's,ODGen,Silent-SpringSemgrep,CodeQL,ESLintArteauArteau,Nodestn/aArteau, Ob-jLupAnsysODGen,CodeQLAnalysis
FailureNoInnit taint
ow trackingunknown(closedsource)Path and
object explo-sionsInniteentrypoint
execution= Artreau'sExponential
node model-ingPerformance0.51 sec/-package1.23 min/-package6.17 sec/-package9.36 min/-package8.5 sec/pack-age12.45 sec/-package1.13 min/-packageNew CVEs66 (25 criti-
cal, 34 high)25 (19 criti-cal)24191660Table 2.1: Comparison of analysis approaches in prototype pollution related works
2.4 Ethical Considerations
Our work has obvious ethical implications, as we identify zero-day vulnerabilities in
widely-used software packages, which may affect many server-side applications that use
such vulnerable packages, and of course, in-turn, users of those services. We reached out to
developers of each vulnerable package, with all the details of the vulnerabilities (including
the exploits), either through emails or dedicated bug-reporting platforms; messages are re-
sent if no response is received in 3 days. In total, we wait for four weeks before we make the
vulnerability public through a GitHub advisory, and submit a CVE. One developer (for the
package `node-opcua-alarm-condition@2.134.0') requested for two additional weeks, and
we waited about two months in that case before making the advisory public. Maintainers
of 4 packages also awarded us bug bounties.
In terms of disclosure timeline, for the 62 CVEs (listed in the Appendix), 21 were
20

--- page 30 ---

published following an email notication, with an average of 127 days. For the remaining
41 CVEs—where no email notication was received, we used the publication date listed oncve.org
(which may initially contain only the CVE ID): the average delay was 106 days.
Only 3 CVEs were published under 60 days: 2 were xed soon after our notication, for
another, the developer xed the issue in a new version (not the reported version). 20 CVEs
were published after 79 days (in batch) for which no developer responses were received.
We resort to a 30-day timeline (from an initial 90-day timeline), as we observed that CVEs
take long before they are made public, and some developers x a bug only after a CVE
becomes public. We always allow the affected developers to take longer if needed, and
have received no objections from developers so far.
Note that some packages are not patched when we make the vulnerability public, for
various reasons: some developers take months to apply a x (recall Fig.4.2); and some
do not want to x the bugs as the vulnerable entry points are not listed in the package
documentation (i.e., not expected to be used by application developers). Attackers may
take advantage of these publicly listed but unpatched vulnerabilities. However, we believe
that application developers using such vulnerable packages should also be aware of these
vulnerabilities, so that they can nd a non-vulnerable alternative—to reduce harm on their
services and users.
21

--- page 31 ---

Chapter 3
Methodology and Implementation
3.1 Overview
Fig.3.1presents our system architecture. Our tool, Bullseye, operates in three main
stages: initial setup, exploit input generation and guided execution, and vulnerability sum-
mary renement. In the initial setup stage (Sec.3.2), we prepare the target environment for
analysis, which takes a list of Node.js packages for evaluation and processes each pack-
age iteratively. During each iteration, Bullseye locally installs the package using Node.js
Package Manager (NPM) to ensure the package's codebase and necessary dependencies
are available. The core task in this stage is to dynamically import the package and extract
all its entry points. In the second stage (Sec.3.3), Bullseye dynamically executes entry
points to inspect the presence of vulnerabilities, identify their locations, and determine the
specic exploit inputs that trigger them. In this stage, we leverage the package's testsuites,
which provide essential information about how each entry point function is invoked under
various scenarios, and use such test inputs for crafting our exploit input candidates. With
the crafted exploit inputs, Bullseye runs the target NPM package, and checks for indica-
tions of prototype pollution. In the nal stage (Sec.3.4), we cross-reference the detected
22

--- page 32 ---

vulnerabilities with existing CVEs, and identify if the detected vulnerability is a poten-
tial zero-day or already conrmed. For zero-days, we automatically create a vulnerability
summary report.
3.2 Stage 1: Initial Setup
The setup process begins by reading a dataset of target packages. For each package,
we use NPM CLI command to install its latest published version, including all dependen-
cies listed in the package metadata le (package.json). We also clone the source code of
the package from its repository link, obtained via the NPM Registry API (registry.
npmjs.org) as we observed that some packages do not include testsuite les in their
bundled NPM version. Next, we identify the package's testsuites and modules with entrypoints.
Testsuite identication.
We locate all testsuite les within the Node.js package. While
the package.json le may indicate these les under the “test” eld, we noticed many pack-
ages do not follow this convention; variations include: the test le name matching the
function (e.g., merge.test.js for testing the function
merge
), or the full path to the test
le matching the function's path name (e.g., assign/object/merge.js for testing the functionassign.object.merge
). Moreover, to be distinguished from the package's modules,
test les usually have a sufx or a prex. We identied commonly-used keywords (e.g.,
test, spec, index), and how they are added to the test le's name, such as using a dash or dot
(e.g., merge.test.js, spec-merge.js). Additionally, these keywords could also be in a parent
directory containing the test le (e.g., spec/merge.js). To locate common path patterns,
we rst select 10 packages from a set of 100 vulnerable Node.js packages (from Silent-
Spring [26]), and create path patterns to locate their test les. Then we apply these patterns
on the rest of the packages, and select another 10 packages for manual analysis where no
testsuites were detected, and update our list of patterns (if new patterns are found). We
24

--- page 33 ---

continue this process until we curate a comprehensive path patterns that can locate all the
test les in the 100 Node.js packages. Then, we use the glob library [29] to run the patterns
on the package's directory to fetch the matched test les; see TableA.3(in the appendix)
for these patterns.
Comprehensive module import.
While the native importing functions (e.g.,
require
,
import
) can be used to import a package, we noticed that, in many cases, importing
cannot be fully accomplished with a uniform pattern (e.g., sorely rely on
require
). These
cases, as discussed below, if not handled properly, may signicantly affect the import of
modules under test (i.e., leading to low code coverage).
(1) Different module systems specify their own import functions. In most cases, a
package supports only one module system, such as CommonJS or ES modules. This re-
quires using the correct import method:
require
for CommonJS and
import
for ES
modules. However, some packages offer universal support for multiple module systems.
They provide different versions of the same module, distinguished by le extensions (e.g.,
module.cjs.js, module.esm.js, module.es2015.js). These versions are then dened in pack-
age.json. In this scenario, relying on a single import method may prevent detection of
potential vulnerabilities in other versions.
To support different module systems, we inspect the package.json le for keys such as
`exports.import', `exports.require', `module', `main', and `jsnext:main'. Next, we use the
appropriate native import function for each module type, e.g.,
require
for modules under
`exports.require' and `main', and
import
for those under `exports.import', `module', and
`jsnext:main'. Additionally, for universal packages that export functions with the same
name, we assign a property to label the type of newly imported object (e.g., esm: lib.esm,
cjs: lib.cjs, es6: lib.es6). These labels are then used to cover different versions of the same
module in the package.
(2) Some packages do not explicitly index modules in package.json. These modules can
25

--- page 34 ---

still be accessed with relative paths (e.g.,
var lib=require(`lib/module.js')
).
For such cases, we recursively traverse the internal modules of the package for JavaScript
les, and import each module.
(3) Some modules are imported implicitly. Some packages do not require assigning the
imported module to a variable. Instead, they use a bootstrap mechanism that dynamically
creates a global namespace object when the package is imported. For this case, we store a
copy of the global object before and after we import the package. Then, we compare the
difference of both copies, which shows the newly imported package in the global object (if
any).
Entry point identication.
To extract the entry points from an imported object, we iterate
over the properties of this object, storing all function-typed properties. We apply the loopstatement
for..in1to perform this process (e.g.,
for (const fn in lib)
). During
this process, we discovered that the structure of the imported objects varies. To access
these objects and retrieve their entry points, we execute two enumeration processes. One
uses the
for::in
statement, and the other uses
Reflect.ownKeys
. Each process returns
the rst-level properties of the imported object. Next, we recursively enumerate each of
these properties. Each time we inspect the property type to decide whether to store this
property (if it is a `function-typed'), or further recursively enumerate it (if it is `object-
typed'). We encounter the following issues while directly traversing the properties of the
imported object: (i) in some cases, enumerating function properties may reveal additional
nested functions; (ii) some functions are only accessible as properties of a class.
To address (i), after applying
Reflect.ownKeys
, we check if the a
function-typed
value can be further explored before we nally store it. To address (ii), we check if the
property has the following built-in class methods:
get
,
set
and
has
. The presence of
these methods indicates the value type of class, which potentially contains entry points as1https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
Statements/for...in26

--- page 35 ---

properties of a class. If so, we enumerate its methods by accessing its prototype property(e.g.,
Reflect:ownKeys
(
prop:prototype
)
).
3.3 Stage 2: Exploit Input Generation and Guided Execu-
tion
In this stage, Bullseye examines if there is any entry point in a given Node.js pack-
age that can trigger prototype pollution vulnerability. To generate exploit input candidates,
we need prototype pollution-related values and the function-specic argument structures.
Thus, we enumerate a list of input fragments (seed corpus), which are values that can effec-
tively trigger prototype pollution vulnerabilities as found in prior work [4,31]. However,
prior work uses these values in xed inputs, disregarding the entry function's signature/ar-
gument structure. We thus leverage the package's own testsuite to identify test inputs for
each entry point, and determine expected argument structures and values. We then mutate
this argument structure with our predened fragments (seed corpus).
Seed corpus curation.
We prepare a list of prototype pollution fragments to serve as seeds
for generating exploit input candidates. First, we reviewed prior work [4,31], specically
the exploit input list (see TableA.4in the appendix), and selected 20 fragments that we
could use as primitive values for generating exploit input candidates. We curated these
fragments in subsets, each of which is tagged with a data type (e.g., array, object, string).
Moreover, we dened special data types (e.g., N-array, S-string) to address the types that
are often found in prototype pollution payloads. For example, some string inputs in pro-
totype pollution payloads come with a specic pattern: slash, dot, colons, or bracket (e.g.,
“/obj/prop”, “obj.prop”, “obj:prop”, “obj[prop]”). To properly generate exploit input can-
didates, we need to use the right string based on the matched one we found in the testsuite.
27

--- page 36 ---

Thus, we distinguish each of these types with special names respectively: S-string, D-
string, C-string, A-string. We also dene special array type: `N-array' for the exploit inputs
that take multi-dimensional arrays. This special type covers inputs in the formats such as
[[FRAG1],FRAG2], [[FRAG1,FRAG2], VALUE]. We list our curated seeds in Table3.1.
Note that we do not include any values with the type of integer or boolean as proto-
type pollution fragments, because they cannot be exploited for prototype pollution directly.
Furthermore, we do not take the constructor.prototype fragment (found in TableA.4, in
the appendix, as BADJSON2 at number 17, 18, 19, and string values in 21, 23, 30, 33,
38, 42), as we noticed the same effect of usingprotoand constructor.prototype, which
causes redundant vulnerability triggers. Similarly, we exclude le fragments (e.g., 43, 44
in TableA.4, in the appendix), as they did not yield results when tested on 60,000 Node.js
packages [31].
We also expanded the list with missing fragments by prior work (such as the fragments
6, 13, 15, and 18 in Table3.1). These fragments allow discovering vulnerabilities that
prior work cannot nd. For example, fragment 6 triggers a new vulnerability at `acces-
sors/set.js:37' in the package `@cahil/utils@2.3.2', because the vulnerable code requires
an array with three elements to access the enclosed conditional branch.
Test input extraction.
In this step, we extract all test inputs that relate to our entry points.
We use Abstract Syntax Tree (AST) to locate the entry point of interest and extract all
related test cases. The tree representation of the test le's code facilitates tracking the
data ow from one variable or function call to another. For instance, assume that node A
represents the declaration of a variable p (e.g.,
var p
); node B represents the assignment
of a value to p (e.g.,
p = 1
); and node C represents the use of p as an input to a function
call (e.g.,
fun(p)
). Therefore, in the tree structure, edges connect these three nodes
sequentially. If the function
fun(p)
is an entry point, we can locate it as node C, and
determine the type and value of variable p, by tracing node B pointing to C, and further
28

--- page 37 ---

No. Type Seed Input1 object
fg
2 object
fg
.proto3 object
f
“proto.pollutedKey”:“pollutedValue”
g
4 object JSON.parse('
f
“proto”:
f
“pollutedKey”:“pollutedValue”
gg
')
5 array [“proto”, “pollutedKey”]
6 array [“proto”, “pollutedKey”, “pollutedValue”]
7 N-array [[“proto”], “pollutedKey”]
8 N-array [[“proto”], “pollutedKey”, “pollutedValue”]
9 N-array [[“proto”, “pollutedKey”], “pollutedValue”]
10 N-array [[“proto”], [“proto”], “pollutedKey”]
11 string “proto”
12 S-string “/proto/pollutedKey”
13 S-string “/proto/pollutedKey=123”
14 D-string “proto.pollutedKey”
15 D-string “proto.pollutedKey.pollutedValue”
16 D-string “proto.pollutedKey=123”
17 C-string “proto:pollutedKey”
18 C-string “proto:pollutedKey=123”
19 A-string “proto[pollutedKey]”
20 A-string “proto[pollutedKey]=123”Table 3.1: Bullseye's seed corpus, organized in data types and special formats.
node A pointing to B.
We start from the root node and traverse all nodes in the tree. During the traversal, we
examine function call nodes (node C) and check if this function is an entry point. If the
called function is an entry point, we backtrack all nodes pointing to the node representing
the call to the entry point, as these nodes represent the input variables (node B) of the
entry point function. Additionally, for each input variable node (node B), we continue
backtracking until the variable is initially declared (node A). By backtracking through these
nodes, we can determine how an entry point's input variable was declared, how it was
updated, and its value is when the entry point is called.
Entry points identication in testsuites.
When analyzing testsuites, we found that other
than the package's entry points, testsuites can also import and execute functions from other
29

--- page 38 ---

modules (e.g., calling the
assert
function to compare results). Therefore, we need to
ensure that we only track the entry points belonging to our target package and ignore un-
related functions. To achieve this, we collected the path patterns of import modules from
the testsuites of 100 Node.js packages (from the same packages mentioned in Sec.3.2).
We curated these patterns into a list (see TableA.2). During the testsuite processing, if we
identify an import path matching one of these patterns, we label the assigned identier to
track the point where the related entry point is called. For example, consider that a function
is called in a testsuite as a property of the imported identier (e.g., lib.fun(p)). We identify
the importing node in the AST (e.g.,
import lib from "../index.js"
), which
contains one of our curated import paths. As we spot this line, we label this identier (lib),
and traverse the tree until we nd the call expression node that matches the one we labeled
(lib.fun(p)); we then store the values of test input's arguments. These records are further
utilized to generate the exploitable input candidates.
Exploit input generation.
For each test input of an entry point, we synthesize a batch of
input candidates. Each candidate consists of a valid sequence of arguments for the entry
point. In each candidate, one of its arguments is replaced by a value from Table3.1, serving
as an effective payload to trigger potential prototype pollution. Recall that we record the
data types and values for each test case, from which we generate a batch of candidates.
Given a test case with a sequence of arguments, we check the type of each argument. If
the type of an argument matches the type of an entry in Table3.1, the original value of the
argument is replaced by the value in that entry. We count this new sequence of arguments,
with one value replaced, as a candidate. We generate a batch of candidates in a
pairwise
manner [28] to cover as many potentially effective payloads as possible. We explain this
more using our motivating example.
In Listing1.2, a test case with three arguments (
fg
, a,
f
deep: true
g
) is served
into entry point
merge
, where `a' is an object dened as
const a =
f
a: 1, b:
30

--- page 39 ---

2, c:
f
a: Boolean
gg
. We rst identify the type of rst argument
fg
as object, and
then nd all entries in Table3.1that are also typed as `object' (# 1, 2, 3, 4). Then we replace
the original argument value with the seed input values of these entries to generate our candi-
dates. In this case, we have four candidates targeting the rst argument: (
fg
, a,
f
deep:
true
g
), (
fg
.proto, a,
f
deep: true
g
), (
f
``proto.pollutedKey":
``pollutedValue"
g
, a,
f
deep: true
g
) and (
JSON.parse('
f
``proto":
f
``pollutedKey": ``pollutedValue"
gg
'), a,
f
deep: true
g
). Sim-
ilarly, another four candidates will be generated by replacing the value of the second argu-
ment in the original test case, and another four of the third argument. We thus have a batch
of 12 candidates mutated from this original test case for the entry point
merge
. If there is
another test case for
merge
in the testsuite, a new batch of candidates will be generated
based on that test case. In the end, we execute the entry point (
merge
) with all candidate
inputs.
Guided execution.
After collecting the entry points and generating the exploit input can-
didates, Bullseye executes each entry point with its relevant exploit input candidates and
actively monitors for signs of prototype pollution. This process ensures precise detection
by observing the runtime's behavior in real-time and identifying the vulnerable code that
enables prototype pollution. However, using a shared environment between Bullseye and
the tested function is risky, as we noticed cases where Bullseye stopped working because
of an endless loop invoked by the tested function. This is one of the drawbacks of prior
tools [4,31], as they cannot complete the scan on a dataset if any of the tested functions
invoked an endless loop. We addressed this challenge by using Node.js's VM module,2which can be used to trigger a timeout for long-processing functions. We set the timeout to
100 ms for executing an entry point against each exploit input candidate, which we found
to be enough for executing any function without innite loops.32https://nodejs.org/api/vm.html3
We found only 6 packages with such loops: gammautils@0.0.81, locutus@2.0.11, mout@2.0.0-alpha.1,
31

--- page 40 ---

An obvious sign for prototype pollution is the inheritance of the injected property from
any object in the running application, including the empty object. However, as we ob-
served, the injection can occur in other places. (1) The polluted property can be added
with an empty value (e.g.,
f
polluted
: \"
g
), or under another object (e.g.,
f
someObj
:
f
polluted
:
true
gg
). (2) The key and value in the injection combine as a key name of an ob-
ject (e.g.,
\
polluted
=
true
" :
fg
), making a simple check ineffective (e.g.,
fg
:polluted
).
To solve these cases, we introduce two side-effect checking oracles: recursive prototype
chain inspection and prototype chain differential checking to detect pollution in nested/-
complex objects. In the rst oracle, we recursively access the prototype chain, looking for
the injected property by matching our predened key-value pairs with each property we
read from the prototype chain. Next, the second oracle is used to detect the side-effect
even if the key-value pair of the polluted property is mutated by the target function. We
compare the prototype chain before and after the injection attempt to reveal the changes
occurred due to the pollution. Specically, we clone the global prototype (e.g., usingObject.getPrototypeOf
) before and after we execute the exploit. Then, we read
the property names from each clone (e.g.,
Object.getOwnPropertyNames
). After
having both arrays of properties, we apply Array's
find
method to nd the newly added
property.
Beside detecting prototype pollution side-effects, we also attempt to identify the vul-
nerable code line. We use Proxy [24] to intercept the pollution attempt by customizing the
set
handler to detect the polluted property. Specically, we modify the
set
handler of
an empty object's prototype. Then, we replace the empty object at the exploit input under
execution with the proxied object. Upon the entry point execution, if the prototype pollu-
tion occurs, the runtime returns to
set
handler and we check if the property to be added isnis-utils@0.6.10, node-forge@0.9.0, nodee-utils@1.2.2. E.g., the function
shufe
in gammautils@0.0.81
has the condition
while (0 !== currentIndex)
, where
currentIndex
is the array length of the
argument. If the supplied argument is not an array,
currentIndex
becomes
NaN
, consequently the while
loop is always true.
32

--- page 41 ---

protoor
polluted
. In that case, we use the runtime's call stack to get the last executed
line of code before Bullseye, which represents our sink line. Note that this approach only
works when there is an object argument in the exploit candidate in order to trigger the
set
handler on the pollution attempt.
3.4 Stage 3: Vulnerability Summary Renement
In the nal stage, we generate a vulnerability summary based on the results of the
guided execution. We include the location of vulnerable sinks (if available), the package's
entry points, and the executable proof-of-concept exploits in detail. We also compare these
vulnerabilities with previously disclosed ones. Specically, we retrieve publicly disclosed
vulnerabilities for a target package as identied by their CVE IDs (if any). We use the
GitHub Advisory Database4for CVE data, through the Octokit API client [9], querying
both the package name and the CWE ID CWE-1321, which corresponds to prototype pol-
lution vulnerabilities. The API returns CVEs related to prototype pollution for the target
package, including detailed descriptions that may identify affected components (e.g., func-
tion names, code snippets, line numbers, or le paths). We cross-reference these details
with the recorded entry points and sink locations. When there is a match, the relevant CVE
ID is added to the nal vulnerability report.
We determine whether a nding qualies as a zero-day as follows. We automatically de-
duplicate by comparing the entry points of discovered vulnerabilities with those in known
prototype-pollution CVEs, when a CVE includes entry point details. Otherwise, we con-
servatively assume that the CVE might cover our nding and treat it as a duplicate. That
is: a nding is classied as a zero-day only if, for the affected package, no CVE shares the
same entry point as we found, or if no prototype pollution-related CVEs exist for the pack-
age. This conservative strategy minimizes duplicate reports at the expense of potentially4https://github.com/advisories33

--- page 42 ---

missing some true zero-days.
3.5 Implementation
Bullseye is implemented in JavaScript, comprising 2940 lines of code. We integrate
several key components as follows. The package setup leverages JavaScript's
import
for
dynamic and asynchronous loading of the target package. To enumerate exported func-
tions, Bullseye applies
Reflect.ownKeys
, enabling identication of all entry functions
within the imported package object. Test inputs are extracted by locating relevant test les
within the package. We curated a list of path patterns to cover all possible locations of test
les in the Node.js packages (e.g.,
=
 
=
 f
test; spec
g 
:js
), based on the observation of
100 Node.js Packages. Then we use the Node.js library Glob [11] to match these patterns
in the package's directory (TableA.3in the appendix). Test les are parsed into ASTs us-
ing Acorn [2], and traversed with Acorn-walk [1] to extract the relevant test inputs for each
entry point.
We rely on Microsoft PICT [21] to effectively produce exploit input candidates. For
each test case, we create a PICT module with the test inputs and seeds. The PICT module
combines them into a series of exploitable inputs. A PICT module consists of a set of ob-
jects, where in each object we dene the property and its possible values. Thus, we assign
the input's arguments from the test input and the selected seeds from Table3.1(i.e., by
matching the type) as the “values” of objects. Meanwhile, we generate the labels that con-
tain the order and data type of the input arguments, and set these labels as the “properties”
of the objects. This format allows reassembling the results from PICT into usable inputs.
For instance, for the input
(\
test
"
;
fg
; true
)
, we generate the properties: string1, object2,
boolean3, respectively. Given the rst property, its corresponding value comes from the test
input “test”, along with the type-matched string seeds. The nal object for the rst property
becomes as:
f
property:`string1',values:[`test',`proto']
g
.
34

--- page 43 ---

We use Proxy [24] to intercept the modication attempt on Object.prototype. Since
the global prototype is immutable, we wrap a Proxy on the prototype of an empty object,
then we modify the
set
handler in the Proxy to detect if the function we execute attempts
to modify the prototype. If the modication attempt is detected, we log the stack trace
using JavaScript's error stack (
Error.prototype.stack
[22]), which includes the
sink location (i.e., last executed line after the modication attempt).
Furthermore, we use JavaScript VM [25] to mitigate innite execution loops and un-
expected behaviors that might affect the runtime during the execution. We specically userunInContext
to test the entry point function with a timeout threshold of 100 millisec-
onds. The target functions are dynamically invoked using
Reflect.apply
.
To ensure the integrity of the runtime of each package, we run Bullseye in contain-
ers. These containers are dynamically created for each package in the loop. Instead of
using Docker's CLI, we use the `dockerode' library to communicate with the Docker en-
gine through the Docker API. This approach also improves the efciency of Bullseye. For
each package under test, our system automatically creates a container based on an image
we prepared with Node.js and dependent libraries for Bullseye. This container takes the
package information as an input and run Bullseye. The container then listens for the detec-
tion results from Bullseye and records them in a log le. At the end of the execution (or if
a timeout is reached), the container is removed, freeing up the host's resources.
We use the package `p-limit'5to control parallel executions between containers. Specif-
ically, we wrap each container execution in a
limit
call, ensuring that only a specied
number of containers, dened as an argument, run at the same time. It also prevents system
overload from too many parallel processes. This concurrency conguration allows us to
manage resource usage effectively while still taking advantage of parallelism to process
multiple packages simultaneously.5https://www.npmjs.com/package/p-limit35

--- page 44 ---

Chapter 4
Evaluation
We focus on answering key research questions that assess Bullseye's ability to detect
known and unknown/zero-day prototype pollution vulnerabilities.
RQ1:
How effective is
Bullseye in detecting previously reported prototype pollution vulnerabilities? How does
it compare with existing tools?
RQ2:
How effective is Bullseye in uncovering zero-day
prototype pollution vulnerabilities in the wild?
RQ3:
How effective are existing tools in
detecting zero day vulnerabilities uncovered by Bullseye?
RQ4:
How effective are various
components of Bullseye in detecting zero day vulnerabilities?
4.1 Experimental Setup
Environment.
We performed our evaluation on a physical machine running Ubuntu 22.04,
equipped with a 16-core AMD Ryzen Threadripper 2950X CPU (released in 2018), 64 GB
of RAM, and 8 TB of SSD storage. We employed Docker version 24.0.5 for our analysis.
The containerization ensures that our physical device remains unaffected by the Node.js
packages under inspection. Meanwhile, it isolates the process of running each package,
preventing any version conict for common dependencies. We limit the maximum number
of containers running in parallel to 64 (for maximum utilization of our CPU).
36

--- page 45 ---

Datasets.
To evaluate the effectiveness and accuracy of our system against existing tools,
we use the datasets provided by the authors of those tools. We also test Bullseye on real-
world Node.js packages, focusing on widely used packages, with at least 10,000 weekly
downloads, which we can get by querying the package's metadata from the NPM registry
API. However, because of the API rate limit in NPM,1we only fetch the maximum allowed
packages every day and save only the ones with 10,000 weekly downloads, repeating this
process every day. Eventually, we ran the script continuously for three months (June-
August, 2024), curating a dataset of 44,513 packages. We also tested randomly-chosen
packages (starting from early 2024) irrespective of their download rates, at various stages
of our tool development, and eventually selected 5,879 of these packages for evaluation, all
of which had a weekly download rate less than 10,000. These two datasets allowed us to
understand prototype pollution vulnerabilities in packages with different popularity levels.
Baseline tools.
We benchmark Bullseye against prominent dynamic and static analysis
approaches. For the dynamic analysis baseline, we use two fuzzing tools from: Arteau [4],
and Zhou and Gao [31]. For the static analysis baseline, we use the tool proposed by Li
et al. [18], ODGen, which is a general-purpose tool for detecting JavaScript vulnerabilities
including prototype pollution. The second static baseline is Silent-Spring [26], which uses
four different CodeQL queries based on the scope for scanning the package. To have a fair
comparison, we choose “Priority queries/Exported Functions” which is the most relevant
to the scope of Bullseye, as we identify a vulnerability by nding the injected property, and
we only test exported functions.1https://blog.npmjs.org/post/164799520460/api-rate-limiting-rolling-out.
html37

--- page 46 ---

4.2 RQ1: Detection of Past Vulnerabilities
First, we benchmark Bullseye against Node.js packages previously reported with pro-
totype pollution (as found in our baselines). We compare our results based on the vulnera-
bility details provided by the baseline tools, e.g., the entry point function, the path or line
number of the sink. If the vulnerability details are partially provided, such as a sink loca-
tion, we match it with the sink location reported by Bullseye. For the results where Bullseye
does not provide a sink line, we manually debug the generated exploit by Bullseye to nd
the sink line and compare it with the baseline.
Arteau [4].
We evaluate Bullseye against the 15 vulnerable packages listed by Arteau
(including the package version and the vulnerable entry points). Bullseye was able to detect
prototype pollution in all packages. In addition to all the vulnerable entry points from
Arteau's list, Bullseye found 3 new vulnerable entry points (
pick
, and
updateWith
in
`lodash@4.17.4', and
clone
in `deap@1.0.0').
Zhou and Gao [31].
From the 65 packages listed by the authors with vulnerabilities, only
6 packages come with a CVE ID.2Thus we test Bullseye against these packages, as we
can use the details of these vulnerabilities from their CVE details for comparison. All
vulnerabilities were detected by Bullseye.
ODGen [18].
We test Bullseye against 19 packages with prototype pollution vulnerabilities
listed by the authors. For comparison, we used the benchmark (for the same vulnerabili-
ties) from Silent-Spring [26] that includes the sink lines and the PoC les that trigger the
injection for each package. If the exploit case generated by Bullseye does not contain a
sink location, we ran the entry point with the same exploit case in debug mode, and man-
ually traced the execution until it reached the same sink location from the benchmark list.
Bullseye detected 26/38 vulnerable sink locations (missing 12, with no false positives). We2
They are: `safe-eval@0.4.1', `atnest@1.0.0', `collection.js@6.7.11', `rangy@1.3.1', `progress-
bar.js@1.1.0'.
38

--- page 47 ---

manually checked all false negatives (FNs), and found that infeasible attack vectors and
complex exploits are the primary reasons for missing them (detailed in AppendixA.1).
Silent-Spring [26].
We evaluate Bullseye against Silent-Spring using the 100 of Node.js
packages listed by the authors. For vulnerabilities without sink locations, we checked them
manually as we did for comparison with ODGen. Table4.1summarizes our results. After
manually checking the installed packages and executing all PoC les, Bullseye is able
to detect 92/134 vulnerabilities. The primary reasons for FNs include complex exploits,
infeasible attack vectors, and unknown payload patterns (detailed in AppendixA.1). We
also found 4 apparent false positive cases. We tested the PoC (CVE-2020-28271) for the
package `deephas@1.0.5' and noticed that the exploit only affects the target object but not
the global prototype. For the package `dot-object@2.1.2' (CVE-2019-10793), we veried
the disclosed exploit,3and found that the agged sink locations in the ground truth are
unexploitable.
Bullseye also uncovered 24 unknown sink locations in 12 packages in the Silent-Spring
ground truth dataset. 17 of these sink locations in 9 packages are conrmed zero-days (7
remaining sinks in 4 packages have already been reported in CVEs). We contacted devel-
opers of these 9 packages with our vulnerability reports. 5/9 packages remained unpatched
in their latest versions (as checked on July 22, 2025).Tool Total sink TP FN FP Duration (sec)Bullseye 134 92 42 0 371
Silent-Spring 134 112 22 113 1850Table 4.1: Overall detections results for the 134 vulnerable sink locations from the selected
100 packages.3https://security.snyk.io/vuln/SNYK-JS-DOTOBJECT-54890539

--- page 48 ---

4.3 RQ2: Uncovering Zero-Day Vulnerabilities
We run Bullseye on 44,513 packages with at least 10,000 weekly downloads (labeled
as high-DL packages), and 5,879 randomly chosen packages (with less than 10,000 weekly
downloads, labeled as low-DL packages). In total, we detected zero-day vulnerabilities
in 290 packages (250 from high-DL and 40 from low-DL packages), in 807 unique entry
points (655 from high-DL and 152 from low-DL packages), and a total of 1,172 exploitable
test cases (950 from high-DL and 222 from low-DL packages). Note that each entry point
represents an independent attack vector; to avoid overwhelming the developers, we share
only one test case for each entry point, and to reduce CVE reporting (a manual process viacveform.mitre.org), we submit only one CVE per package (by grouping all vulner-
abilities in a package if it has multiple ones). Many vulnerable packages are widely used;
see Fig.4.1.Figure 4.1: Distribution of downloads for 250 vulnerable packages (from the high-DL
44,513 packages).
40

--- page 49 ---

We responsibly reported all our ndings to the respective package maintainers, and
submitted for CVEs. As of July 22, 2025, we are assigned 149 CVEs, with 66 published
CVEs. Out of the published CVEs, 25 are marked as critical, and 34 as high, and 7 as
medium; see TableA.1(in the appendix) for the critical and high severity CVEs. From
the 250 high-DL vulnerable packages, 118 CVEs have been assigned; 35 of them have
CVE scores published (7 rated as critical, 26 high, and 2 medium). From the 40 low-DL
vulnerable packages, 31 CVEs have been assigned and made public with severity scores
(18 rated as critical, 8 high, and 5 medium).
Overall, our feedback received positive responses, and apparently, led to the resolution
of security issues in several packages. As of July 22, 2025, 75 developers have responded
to us and 31 of them conrmed xing the reported vulnerabilities. Additionally, one devel-
oper noted that the vulnerable entry point was not mentioned in the ofcial documentation,
suggesting it should not be used by developers (even though it is a valid entry point). Some
developers did not x the bug for the package version we reported, but xed it in a new ver-
sion (assuming the older version should not be used by application developers). For some
old packages, the developers refused to x the bugs even though these packages have high
download rates. We also tracked the status of vulnerable packages maintained by develop-
ers who did not respond within 90 days after our email notication. We found that 11 of
these packages appear to be unmaintained (i.e., no updates for more than a year, no active
response to public issues on GitHub). Notably, for one package, the developer archived
the GitHub repository and marked it as deprecated one month after we sent our notication
email. In addition, four other packages were xed after the relevant CVEs were publiclydisclosed.
As we check the latest versions of the vulnerable packages again with Bullseye (July
22, 2025), we found that 129/290 packages were no longer vulnerable. See Fig.4.2for
both the CVSS severity (left bars) and weekly download counts (right bars) of 23 high-DL
41

--- page 50 ---

packages.<1 Week2-4 months6-12 monthsNot Fixed51015Package count24 249 11238 13 22 1110K 50K50K 100K100K 500K500K 1MCRITICALHIGHMEDIUMFigure 4.2: Patching status distribution of 23 high-DL packages by CVSS severity ratings
(left bar), and weekly DL counts (right bar). To determine if/when vulnerabilities were
xed, we tested all versions of these packages released after our reporting.
To measure the performance efciency of Bullseye, we recorded the running time
of each package when we tested both low-DL packages (5,879) and high-DL packages
(44,513). Bullseye employed a parallel execution mode, allowing multiple packages to be
tested simultaneously. In our experiments, we excluded download and installation time,
and used 64 containers to run in parallel, each with one test package. We rst evaluated the
5,879, where all packages completed testing within 50 minutes. Subsequently, we tested
the packages in 44,513, which completed in 6.3 hours. With Bullseye's parallel execution,
it took an average of 0.51 seconds per package. For individual packages, Bullseye takes
an average of 32.38 seconds/package for evaluation (min. 2 seconds, max. 531 seconds,
with a standard deviation of 20.63 seconds) In addition, we grouped these packages into
42

--- page 51 ---

30-second intervals; see Fig.4.3. Overall, 98.2% of the packages are completed within 1
minute, and 99.8% within 3 minutes.Figure 4.3: Distribution of packages based on their execution completion times for the
high-DL dataset (44,513 packages). Note that we omit the time ranges with package-countzero.
4.4 RQ3: Detection of Our Zero-Days by the Baselines
For baseline comparison, we randomly selected 40 packages from 137 zero-day pack-
ages where we also have the vulnerable sink information; see Table4.2for results summary.
For dynamic baselines, as they do not report sink lines by default, we embedded our
proxy detector in both (Arteau [4], Zhou and Gao [31]), to get the sink lines. They missed
almost half of the vulnerabilities, a strong justication for our use of testsuite-guided input
augmentation—instead of blindly using the xed exploit inputs as in the baselines. On
the other hand, the dynamic baselines detected a few sink locations that Bullseye could
43

--- page 52 ---

not. For the two sink lines (i.e., vis-util.js: 3255, 3257) found by both baselines, the re-
lated entry point (`deepObjectAssign') is passed as an argument to the test function (e.g.,
test
(
deepObjectAssign;
() =
>
f
==:::
g
), which currently Bullseye cannot parse. For the
sink location at (xe-utils/set.js:53), Zhou and Gao [31] detect this vulnerability with an in-
put that has no matching format in the testsuites, specically the two-dimensional array in
the second argument (e.g.,
(
fg
;
[[proto]
;
\
test
"]
;
\123"
; true
)
. In the testsuites, how-
ever, all three relevant test cases have one-dimensional array at the second argument. For
the two sink lines in cloneextend/index.js (120, 123), the entry point of interest (`extendup-
tolevel') has no matching test cases in the package.
For the static baselines, we note that ODGen has overall low detection rate (8/87 zero-
days). Silent-Spring, on the other hand, has higher detection rate but potentially more
false positives (while still missing 53/87 zero-days). Since static baselines do not generate
exploits to validate the ndings, we are not sure about the 55 sink lines in ODGen and
91 sink lines in Silent-Spring for which Bullseye did not provide a PoC; we label them as
unknown or potential false positives.
Possible failure reasons in baselines.
We found that both Silent-Spring and ODGen of-
ten fail to complete the analysis on complex packages. ODGen's failures are largely due
to limited code coverage and call graph imprecision, while Silent-Spring struggles with
dynamic JavaScript features such as bracket-based function calls and dynamic property
accesses. These observations align with the ndings by Kang et al. [12], and Zhou and
Gao [31], which showed that ODGen's false negatives stem from its exponential growth of
analysis nodes and scope mismatches, and that Silent-Spring's reliance on CodeQL makes
it ineffective for certain dynamic language constructs.
Additionally, we found another important weakness related to incomplete modeling of
built-in JavaScript functions in ODGen. For vulnerable code locations detected by Bulls-
eye but missed by the baselines, we manually replaced the critical lines with semantically
44

--- page 53 ---

equivalent alternatives. For example, in Listing1.1, we replaced
Object:getOwnPropertyNames
(
source
in the property access loop with a direct property access loop, after which ODGen, Arteau,
and Zhou and Gao could detect the vulnerability, indicating that the missed detections are
due to incomplete modeling of certain built-in JavaScript functions (not the vulnerability
logic itself).Tool Detected sink FP/Unknown TP FN Duration (sec)Arteau 45 2 43 44 337
Zhou and Gao 52 5 47 40 498
ODGen 63 55 8 79 28080
Silent-Spring 125 91 34 53 2938
Bullseye 87 0 87 0 875Table 4.2: Baselines' detection performance on our selected zero-days (with a total of 87
ground truth sink locations).
4.5 RQ4: Effectiveness of Bullseye Components
The design of Bullseye consists of multiple components. Recall that in the initial stage,
the loading component traverses all modules contained in the package, and for each dis-
covered module, the enumeration component further enumerates all exported functions as
potential entry points. Then in the guided execution stage with input augmentation, the
test case generation module rst examines the testsuites in each module and synthesizes
pairwise test cases that are specically targeted towards the identied entry points. Each of
these generated test case is then executed within the VM by invoking the entry points. Fi-
nally, the side effect monitor observes any side effects caused by prototype pollution. In our
ablation study, we systematically analyze each component in the initialization and guided
execution stages, by removing/substituting each with a related variant from previous work
(primarily Arteau [4]) to analyze its impact on the overall detection capability.
45

--- page 54 ---

4.5.1 Components in Initial Stage
In this scenario, we explore how the initialization of a package under inspection affects
Bullseye's results. At this stage, both the loading and the enumeration components should
inuence the detection outcomes, as they determine the number of modules discovered, as
well as the number of entry points used for testing within each module.
Therefore, we design the following experiment, in which we modify Bullseye to create
the following variants: (1) in Bullseye
ArteauLoad
, we replace only our loading component
with that of Arteau. (2) in Bullseye
ArteauEnum
, we replace only our entry point enumera-
tion component with that of Arteau. (3) in Bullseye
ArteauInit
, we replace both components
(loading and enumeration) with the corresponding ones from Arteau. Note that Arteau's
tool is not modularized and has a high degree of implementation coupling, which makes
direct component replacement difcult. We thus adapted Arteau's loading and enumeration
strategies within our system to the extent possible. Additionally, to gain more insights into
how our loading and enumeration components enhance our test coverage, we performed
another experiment: for the vulnerable packages discovered by each variant, we executed
only the initialization stage and recorded the number of modules and entry points reached
by that variant.
The results from these variants are summarized in Table4.3. In both the high-download
and low-download datasets, Bullseye outperforms other variants in terms of discovering
vulnerable packages and identifying exploitable entry points. We also noticed an unex-
pected result where the combined variant Bullseye
ArteauInit
yielded more exploitable entry
points than the enumeration-only variant Bullseye
ArteauEnum
. To understand this discrepancy,
we examined Arteau's enumeration logic in detail and found that it stems from a compat-
ibility issue: Arteau's function enumeration cannot effectively traverse modules that we
comprehensively collected from our package loading component. In terms of the overall
enumerated modules and accessible entry points, our loading and enumeration components
46

--- page 55 ---

also produced the best outcomes.
4.5.2 Components in Guided Execution
In this scenario, we focus on the input generation and testing strategies used in Bulls-
eye. Recall that our methodology includes pairwise test case generation, VM isolation,
and side-effect monitoring. To gure out the contribution of these components, we create
the following variants of Bullseye. (1) In Bullseye
NoVM
, we remove the VM that isolates
the execution of each test case. (2) In Bullseye
NoPW
, we remove the pairwise generation
component and rely solely on the xed inputs used in prior work. (3) In Bullseye
ArteauSE
,
we replace only the side-effect detection with Arteau. (4) In Bullseye
NoVM, NoPW
, we re-
move both VM and pairwise generation, while retaining our side effect monitor. (5) In
Bullseye
NoPW, ArteauSE
, we remove pairwise generation and replace our side-effect monitor-
ing with Arteau. (6) In Bullseye
ArteauFuzzy
, we remove all our three components, and keep
Arteau's fuzzing execution. Table4.4summarizes our results. It is evident that each of the
new components of Bullseye signicantly enhances the detection capability, i.e., the use of
more components leads to the detection of more vulnerable packages.Variant # Vuln. Pkg # Exp. EP # Module # Access. EPBullseye 290 818 1353 154889
Bullseye
ArteauLoad
279 518 298 102422
Bullseye
ArteauInit
193 225 298 12870
Bullseye
ArteauEnum
58 68 1353 866Table 4.3: Comparison of Bullseye variants mutated in the initialization stage (for all 290
vulnerable packages). For each variant, we record the number of vulnerable packages,
exploitable entry points (Exp. EP), the number of module paths and accessible entry points
(Access. EP) discovered.
47

--- page 56 ---

Variant # Vulnerable Package # Exploitable Entry PointBullseye 290 818
Bullseye
NoVM
283 711
Bullseye
NoPW
176 357
Bullseye
ArteauSE
164 333
Bullseye
NoVM, NoPW
163 335
Bullseye
NoPW, ArteauSE
157 317
Bullseye
ArteauFuzzy
147 291Table 4.4: Comparison of Bullseye variants mutated in the guided execution stage (for all
290 vulnerable packages)
.
48

--- page 57 ---

Chapter 5
Conclusion and Future Work
To conclude, Bullseye demonstrated that the persistent and pervasive threat of prototype
pollution in the Node.js ecosystem can be addressed with a practical and scalable solution.
By combining static analysis of testsuites with guided dynamic execution and sophisticated
side-effect oracles, Bullseye not only exposed vulnerabilities missed by state-of-the-art
tools, but also avoided false positives—a major hindrance in previous work. The tool's
success in identifying 290 vulnerable packages, many of which are heavily relied upon in
the software supply chain, highlights its practical value. Moreover, the recognition through
CVEs and developer responses conrms Bullseye's real-world impact. This work sets a
new benchmark for precise vulnerability detection in JavaScript environments.
Still, substantial research is needed to further tackle the challenges posed by prototype
pollution. The highly dynamic nature of JavaScript, coupled with the frequent addition
of new language features, continues to frustrate purely static techniques. By releasing
Bullseye's source code, curated datasets, and exploit inputs, we aim to equip researchers
with concrete tools and insights to drive further innovations in this space.
49

--- page 58 ---

5.1 Limitation
The main limitations of Bullseye include the following.
•
Testsuite's inputs extractions.
Our current matching technique relies on pre-dened string patterns, which may lead
to false negatives when path aliases, dynamic imports, or unconventional project
structures are involved. This could be addressed by employing a more generalizable
method, such as analyzing function signatures derived from the codebase, or leverag-
ing static analysis techniques to more accurately attribute function calls to the correctmodule.
•
Exploit candidates generation.
Some complex data structures, such as when test inputs are JavaScript objects with
nested properties, are not handled for efciency reasons. In those cases, both data
and control might be embedded in object properties, which we do not down to create
ne-grained combinations at the level of individual properties.
•
Sink detections.
Bullseye does not detect all sink locations, primarily due to the reliance on passing
the proxied object as an argument to an entry point. This strategy fails in cases where
the target object is not explicitly passed, such as when a variable is dened directly
within the function body. This limitation could be addressed through an instrumented
Node.js runtime.
•
Cross-reference with historic CVEs.
We match entry points and sink locations with GitHub advisories by comparing
function names and sink paths marked with backticks (
`
), as commonly used inMarkdownGuide.org. However, this may miss matches when advisories are not
50

--- page 59 ---

written in Markdown, especially those imported from other platforms. This can be
addressed by using commit links from advisories to match patched code lines with
our detected sink locations.
5.2 Future Work
Future work can explore leveraging large language models (LLMs) to address the cur-
rent limitations observed in Bullseye and enhance its overall detection capabilities andperformance.
• To enhance the matching of entry points with their corresponding function calls in
test suites, LLMs can be utilized to interpret function signatures and capture seman-
tic relationships within the codebase, providing a more exible and context-aware
mapping that goes beyond basic string matching.
• For generating exploit candidates, LLMs can improve effectiveness by comprehend-
ing complex nested data structures within test inputs, enabling the creation of more
precise and ne-grained input combinations.
• To address missing sink location, LLMs could enhance the detection of sink locations
by understanding deeper code semantics and control ow to identify sink locations
not explicitly passed as arguments.
• LLMs can enhance cross-referencing with historic CVEs by understanding various
advisory formats, extracting semantic relationships beyond basic Markdown syntax,
and linking commit histories and patched code segments to the detected sink loca-tions.
51

--- page 60 ---

Appendix A
A.1 Manual Analysis of FNs in ODGen and Silent-Spring
We manually checked all 12 FNs from ODGen, and identied the following reasons for
missing them in Bullseye.
(1) Infeasible attack vectors
: we identied 8 sink locations in
5 packages that need pre-conditions which are not aligned with the package's use cases.
They are: `class-transformer@0.2.3', `dnspod-client@0.1.3', `draft@0.2.3', `eld@1.0.1',
`node-le-cache@1.0.2'. For instance, in the package `class-transformer@0.2.3', the ob-
ject `payload' is modied at its prototype with a new property, which equals the payload
itself (see line 3 in ListingA.2, in the appendix); also the `toString()' property in the pay-
load is assigned with an anonymous function that returns the polluted value.
(2) Complex
exploits
: we identied 4 sink locations where Bullseye failed because of multi-step ex-
ploits (in packages `bayrell-nodejs@0.8.0' and `grunt-util-property@0.0.2'). For example,
to exploit `grunt-util-property@0.0.2', one need to call the method
addExport
as inList-
ing A.1where the function
getClassName
return the payload string that matches the frag-
ment #15 in Table3.1. Bullseye missed the vulnerability as the exploit is not in the pre-
dened xed inputs, and the package has no testsuites to help us generate the exploit.
We identied the following reasons for the 40 FNs from Silent-Spring: complex ex-
ploits (23), infeasible attack vector (7), unknown payload pattern (8), and 4 false positives
52

--- page 61 ---

Listing A.1: function call for addExport in grunt-util-property@0.0.2use.addExport(
fg
,
f
getClassName:function()
f
return ”FRAG#15”;
g
, toString:function()
f
return ”VALUE”;
gg
)Listing A.2: PoC exploit for class-transformer@0.2.3const root = require(”./classtransformer@0.2.3”);
const payload = JSON.parse('
f
”proto”:
f
”polluted”: ”yes”
gg
');
payload.proto.polluted = payload;
payload.toString = function ()
f
return ”yes”;
g
;
root.classToClassFromExist(payload,
fg
,
f
enableCircularCheck: true
g
);(1 in `deephas@1.0.5', and 3 in `dot-object@2.1.2'). As an example of the complex ex-
ploits, in `immer@8.0.0', the exploit need to call a function `enablePatches' before running
the entry point with the payload; this multi-step exploit cannot be executed by Bullseye.
Similar to ODGen, we also noticed that 7 reported vulnerabilities require initializing a
property in the global prototype chain, which we cannot nd in any relevant test cases.
Arguably, such cases should not be considered true positives as the prerequisite conditions
do not align with a package's use cases. Eight vulnerabilities Bullseye failed to detect
because of unknown fragments (i.e., not in our Table3.1). For instance, the package `arr-
atten-unatten@1.1.4' is exploitable through the exploit
unflatten(
f
``proto[polluted]": "yes"
g
);
.
53

--- page 62 ---

A.2 Other Code Listing and Tables
Table A.1: The list of CVEs detected by Bullseye (critical and high severity). Note that in
CVE-2024-38996, four vulnerable packages from the same vendor are grouped.Package CVSS (Severity) CVE IDfast-loops@1.1.3 10 (CRITICAL) CVE-2024-39008
ag-grid-community@31.3.2 9.8 (CRITICAL) CVE-2024-38996
ag-grid-enterprise@31.3.2 9.8 (CRITICAL) CVE-2024-38996
@ag-grid-enterprise/charts@31.3.2 9.8 (CRITICAL) CVE-2024-38996
@agreejs/shared@0.0.1 9.8 (CRITICAL) CVE-2024-39017
@cafebazaar/hod@0.4.14 9.8 (CRITICAL) CVE-2024-39015
@blackprint/engine@0.9.1 9.8 (CRITICAL) CVE-2024-24294
getsetprop@1.1.0 9.8 (CRITICAL) CVE-2024-36575
@jsonic/jsonic-next@2.12.1 9.8 (CRITICAL) CVE-2024-38993
@almela/obx@0.0.4 9.8 (CRITICAL) CVE-2024-36573
@chargeover/redoc@2.0.9-rc.69 9.8 (CRITICAL) CVE-2024-39011
@allpro/form-manager@0.7.4 9.8 (CRITICAL) CVE-2024-36572
mini-deep-assign@0.0.8 9.8 (CRITICAL) CVE-2024-38983
@thi.ng/paths@5.1.62 9.8 (CRITICAL) CVE-2024-29650
@chasemoskal/snapstate@0.0.9 9.8 (CRITICAL) CVE-2024-39010
@75lb/deep-merge@1.1.1 9.8 (CRITICAL) CVE-2024-38986
json-override@0.2.0 9.8 (CRITICAL) CVE-2024-38984
@cdr0/sg@1.0.10 9.8 (CRITICAL) CVE-2024-36580
2o3t-utility@0.1.2 9.8 (CRITICAL) CVE-2024-39013
@cahil/utils@2.3.2 9.8 (CRITICAL) CVE-2024-39014
@ais-ltd/strategyen@0.4.0 9.8 (CRITICAL) CVE-2024-39012
54

--- page 63 ---

Package CVSS (Severity) CVE ID@bunt/util@0.29.19 9.8 (CRITICAL) CVE-2024-38989
@andrei-tatar/nora-rebase-common@1.12.2 9.8 (CRITICAL) CVE-2024-30564
@alexbinary/object-deep-assign@1.0.11 9.8 (CRITICAL) CVE-2024-36582
chartist@1.3.0 9.8 (CRITICAL) CVE-2024-45435
utils-extend@1.0.8 9.1 (CRITICAL) CVE-2024-57077
@intlify/message-resolver@9.1.10 8.9 (HIGH) CVE-2025-27597
@airvertco/frappejs@0.0.11 8.8 (HIGH) CVE-2024-38992
@akbr/patch-into@1.0.1 8.8 (HIGH) CVE-2024-38991
@bit/loader@10.0.3 8.8 (HIGH) CVE-2024-24293
requirejs@2.3.6 8.4 (HIGH) CVE-2024-38998
@apphp/object-resolver@3.1.1 8.3 (HIGH) CVE-2024-36577
uplot@1.6.30 8.2 (HIGH) CVE-2024-21489
dset@3.1.3 8.2 (HIGH) CVE-2024-21529
@apidevtools/json-schema-ref-parser@11.1.0 8.1 (HIGH) CVE-2024-29651
@c3/utils-1@1.0.131 8.1 (HIGH) CVE-2024-39016
@byondreal/accessor@1.0.0 8.1 (HIGH) CVE-2024-36583
@abw/badger-database@1.2.1 7.6 (HIGH) CVE-2024-36581
web3-utils@4.2.0 7.5 (HIGH) CVE-2024-21505
@amoy/common@1.0.10 7.3 (HIGH) CVE-2024-38994
@stryker-mutator/util@8.2.6 7.3 (HIGH) CVE-2024-57085
dot-properties@1.0.1 7.5 (HIGH) CVE-2024-57084
@zag-js/core@0.49.0 7.5 (HIGH) CVE-2024-57079
underscore-contrib@0.3.0 7.5 (HIGH) CVE-2024-57081
xe-utils@3.5.26 7.5 (HIGH) CVE-2024-57074
vxe-table@4.8.10 7.5 (HIGH) CVE-2024-57080
55

--- page 64 ---

Package CVSS (Severity) CVE IDajax-request@1.2.3 7.5 (HIGH) CVE-2024-57076
eazy-logger@4.0.1 7.5 (HIGH) CVE-2024-57075
node-opcua-alarm-condition@2.124.0 7.5 (HIGH) CVE-2024-57086
cli-util@1.1.27 7.5 (HIGH) CVE-2024-57078
module-from-string@3.3.1 7.5 (HIGH) CVE-2024-57072
@ndhoule/defaults@2.0.1 7.5 (HIGH) CVE-2024-57066
@syncfusion/ej2-spreadsheet@25.2.4 7.5 (HIGH) CVE-2024-57064
utile@0.3.0 7.5 (HIGH) CVE-2024-57065
php-parser@3.1.5 7.5 (HIGH) CVE-2024-57071
expand-object@0.4.2 7.5 (HIGH) CVE-2024-57069
php-date-formatter@1.3.6 7.5 (HIGH) CVE-2024-57063
dot-qs@0.2.0 7.5 (HIGH) CVE-2024-57067
@tanstack/form-core@0.19.5 7.5 (HIGH) CVE-2024-57068
@stryker-mutator/util@8.2.6 7.5 (HIGH) CVE-2024-57085
redoc@2.2.0 7.5 (HIGH) CVE-2024-5708356

--- page 65 ---

No. Path Patterns No. Path Patterns1
PACKAGE
14
./
2
./PACKAGE
15
..
3
../PACKAGE
16
../../
4
../../PACKAGE
17
./index.js
5
../
18
../index.js
6
./src/
**
19
./lib/
**
7
../src/
**
20
../lib/
**
8
../../src/
**
21
../../lib/
**
9
../src/index
22
../lib/index
10
../../src/index
23
./src/PACKAGE
11
../src/PACKAGE
24
../../src/PACKAGE
12
./lib/PACKAGE
25
../lib/PACKAGE
13
../../lib/PACKAGETable A.2: List of path patterns used for locating package imports
57

--- page 66 ---

No. Patterns1
fnDir ||
**
|| fnName/fnName ||
*
f
,.,-
g
preSuf.
f
js,coffee,ts,cjs,mjs
g
2
fnDir ||
**
|| fnName/preSuf
f
,.,-
g
fnName ||
*
.
f
js,coffee,ts,cjs,mjs
g
3
fnDir ||
**
|| fnName/packageName ||
*
f
,.,-
g
preSuf.
f
js,coffee,ts,cjs,mjs
g
4
fnDir ||
**
|| fnName/preSuf
f
,.,-
g
packageName ||
*
.
f
js,coffee,ts,cjs,mjs
g
5
**
/packageName ||
*
f
,.,-
g
preSuf.
f
js,coffee,ts,cjs,mjs
g
6
**
/preSuf
f
,.,-
g
packageName ||
*
.
f
js,coffee,ts,cjs,mjs
g
7
f
test,Test,tests,Tests,tests,Tests,spec,Spec,
coffee,Coffee
g
/
**
/
*
.
f
js,cjs,mjs
g
8
*
f
Test,test,Spec,spec
g
*
.
f
js,cjs,mjs
g
9
f
test,Test,tests,Tests,tests,Tests,spec,Spec
g
/
*
.
f
js,cjs,mjs
g
10
*
f
Test,test,Spec,spec
g
*
.
f
js,cjs,mjs
gTable A.3: List of glob patterns used for locating testsuite les in Node.js packages. Nota-
tion: `fnDir': the function path created from the given function name, covering cases where
the test le hierarchy is derived from the function path (e.g., `assign/object/merge.js` from
`assign.object.merge`); `fnName': the last element of a dot-separated function name (e.g.,
`merge` in `util.merge`); `packageName': the name of the package; `preSuf': a set of
keywords used as prexes/sufxes or directories for test les:
f
t,T
g
est
,
f
s,S
g
pec
,
f
i,I
g
ndex
,
f
c,C
g
offee
.
58

--- page 67 ---

No. Exploit Inputs No. Exploit Inputs1 BADJSON 23 “this.constructor.prototype.test”,
fg
, “123”
2 BADJSON,
fg
24 “proto.test”, “123”,
fg
3
fg
, BADJSON 25
fg
, “/proto/test”, “123”
4 BADJSON, BADJSON 26
fg
, “/proto/test”, “123”, true
5
fg
,
fg
, BADJSON 27 “proto.test=123”
6
fg
,
fg
,
fg
, BADJSON 28 “proto:test”, “123”
7
fg
, “proto.test”, 123 29 “proto[test]=123”,
fg
8
fg
, “proto[test]”, 123 30
fg
, “constructor/prototype/test”, “123”, “/”
9 “proto.test”, 123 31 “proto”,
f
“test”: “123”
g
,
fg
, true
10 “proto[test]”, 123 32
f
“proto.test”: “123”
g
11
fg
, “proto”, “test”, 123 33
f
“constructor.prototype.test”: “123”
g
12 “proto”, “test”, 123 34
fg
, [[“proto”], “test”], “123”
13
fg
, BADJSON,
fg
35 [[“proto”], “test”], “123”,
fg
14
fg
, BADJSON, true 36
fg
, [[“proto”], “test”], “123”, true
15 true,
fg
, BADJSON 37
fg
, [“proto”, “test”], “123”
16
fg
, true, BADJSON 38
fg
, [“constructor.prototype.test”], “123”
17 true,
fg
, BADJSON2 39 [“proto”], “test”, “123”
18
fg
, BADJSON2 40 [“proto.test”], [“123”]
19 BADJSON2 41
fg
, [[“proto”], [“proto”], “test”], “123”
20 “[proto]
\n
test=123” 42 [“-constructor.prototype.test”, “123”]
21
fg
, “construc-
tor.prototype.test”, “123”
43 “lename”
->
[proto]
\n
test=“123”
22 “proto.test”,
fg
, “123” 44 “lename”
->
[constructor]
\n
prototype.test=“123”Table A.4: List of exploit inputs curated by Arteau [4] (the rst 12), and Zhou
and Gao [31]. Notation: (
\n
,
->
) refer to a new line-separated payload, and
le's content payload, respectively; (BADJSON, BADJSON2) refer to the follow-
ing JSON-based payloads, respectively: JSON.parse('
f
”proto”:
f
” test ”:123
gg
') and
JSON.parse('
f
” constructor ”:
f
” prototype ”:
f
” test ”:123
ggg
').
59

--- page 68 ---

Bibliography
[1] acornjs, “Acorn AST walker,”https://www.npmjs.com/package/acorn-walk.
[2] ——, “A small, fast, JavaScript-based JavaScript parser,”https://github.com/acornjs/
acorn.
[3] A. AlHamdan and C.-A. Staicu, “Welcome to Jurassic park: A comprehensive study
of security risks in Deno and its ecosystem,” in
Network and Distributed System Se-
curity (NDSS) Symposium
, San Diego, CA, USA, 2025.
[4] O. Arteau, “Prototype pollution attack in NodeJS application,” 2018,
North-sec conference, Montreal, Canada.https://github.com/HoLyVieR/
prototype-pollution-nsec18/.
[5] M. Bentkowski, “Exploiting prototype pollution - RCE in Kibana (CVE-
2019-7609),” technical report (Oct. 30, 2019).https://research.securitum.com/
prototype-pollution-rce-kibana-cve-2019-7609/.
[6] D. Cassel, N. Sabino, L. Jia, and R. Martins, “NODEMEDIC-FINE: Automatic De-
tection and Exploit Synthesis for Node.js Vulnerabilities,” in
Network and Distributed
System Security Symposium (NDSS'25)
, San Diego, CA, USA, Feb. 2025.
[7] E. Cornelissen, M. Shcherbakov, and M. Balliu, “GHunter: Universal prototype pol-
lution gadgets in JavaScript runtimes,” in
Usenix Security Symposium
, Philadelphia,
PA, USA, Aug. 2024.
60

--- page 69 ---

[8] J. Czerwonka, “Pairwise testing in the real world: Practical extensions to test-case
scenarios,” in
Pacic Northwest Software Quality Conference (PNSQC'06)
, Portland,
OR, USA, Oct. 2006.
[9] GitHub.com, “REST API endpoints for global security advisories,”https://docs.
github.com/en/rest/security-advisories/global-advisories?apiVersion=2022-11-28.
[10] T. Houis, S. Jiang, M. Mannan, and A. Youssef, “Bullseye: Detecting prototype pol-
lution in npm packages with proof of concept exploits,” in
Network and Distributed
System Security Symposium (NDSS'26)
, San Diego, CA, USA, Feb. 2026.
[11] isaacs/node-glob, “Glob functionality for Node.js,”https://github.com/isaacs/
node-glob.
[12] M. Kang, Y. Xu, S. Li, R. Gjomemo, J. Hou, V. N. Venkatakrishnan, and Y. Cao,
“Scaling JavaScript abstract interpretation to detect and exploit node.js taint-style vul-
nerability,” in
2023 IEEE Symposium on Security and Privacy (SP)
, San Francisco,
CA, USA, May 2023.
[13] Z. Kang, S. Li, and Y. Cao, “Probe the Proto: Measuring client-side prototype pollu-
tion vulnerabilities of one million real-world websites,” in
Network and Distributed
System Security Symposium (NDSS'22)
, San Diego, CA, USA, Apr. 2022.
[14] Z. Kang, M. Lyu, Z. Liu, J. Yu, R. Fan, S. Li, and Y. Cao, “Follow my ow: Unveil-
ing client-side prototype pollution gadgets from one million real-world websites,” in
IEEE Symposium on Security and Privacy (SP)
, San Francisco, CA, USA, May 2025.
[15] H. Y. Kim, J. H. Kim, H. K. Oh, B. J. Lee, S. W. Mun, J. H. Shin, and K. Kim, “DAPP:
automatic detection and analysis of prototype pollution vulnerability in Node.js mod-
ules,”
International Journal of Information Security
, Feb. 2022.
61

--- page 70 ---

[16] M. Kluban, M. Mannan, and A. Youssef, “On Detecting and Measuring Exploitable
JavaScript Functions in Real-world Applications,”
ACM Transactions on Privacy and
Security (ACM TOPS)
, vol. 27, no. 1, pp. 1–37, Feb. 2024.
[17] S. Li, M. Kang, J. Hou, and Y. Cao, “Detecting Node.js prototype pollution vulner-
abilities via object lookup analysis,” in
ACM Joint Meeting on European Software
Engineering Conference and Symposium on the Foundations of Software Engineering(ESEC/FSE'21)
, Athens, Greece, Aug. 2021.
[18] ——, “Mining Node.js vulnerabilities via object dependence graph and query,” in
Usenix Security Symposium
, Boston, MA, USA, Aug. 2022.
[19] Z. Liu, K. An, and Y. Cao, “Undened-oriented Programming: Detecting and Chain-
ing Prototype Pollution Gadgets in Node.js Template Engines for Malicious Con-
sequences,” in
IEEE Symposium on Security and Privacy (SP)
, San Francisco, CA,
USA, May 2024.
[20] C. Luo, P. Li, W. Meng, and C. Zhang, “Test suites guided vulnerability validation
for Node.js applications,” in
ACM Conference on Computer and Communications
Security (CCS'24)
, Salt Lake City, UT, USA, Oct. 2024.
[21] Microsoft, “Microsoft pairwise independent combinatorial tool (PICT),”https://
github.com/microsoft/pict.
[22] Mozilla.org, “Error.prototype.stack - JavaScript,”https://developer.mozilla.org/
en-US/docs/Web/JavaScript/Reference/GlobalObjects/Error/stack.
[23] ——, “Object.prototype.proto- JavaScript,”https://developer.mozilla.org/en-US/
docs/Web/JavaScript/Reference/GlobalObjects/Object/proto.
[24] ——, “Proxy - JavaScript,”https://developer.mozilla.org/en-US/docs/Web/
JavaScript/Reference/GlobalObjects/Proxy.
62

--- page 71 ---

[25] NodeJS.org, “VM (executing JavaScript): Node.js v22.7.0 documentation,”https://
nodejs.org/api/vm.html.
[26] M. Shcherbakov, M. Balliu, and C.-A. Staicu, “Silent Spring: Prototype pollution
leads to remote code execution in Node.js,” in
Usenix Security Symposium
, Anaheim,
CA, USA, Aug. 2023.
[27] M. Shcherbakov, P. Moosbrugger, and M. Balliu, “Unveiling the invisible: Detection
and evaluation of prototype pollution gadgets with dynamic taint analysis,” in
The
ACM Web Conference 2024 (WWW'24)
, Singapore, May 2024.
[28] K. Tatsumi, “Test case design support system,” in
International Conference on Qual-
ity Control (ICQC)
, Tokyo, Japan, Oct. 1987.
[29] VisualStudio.com, “Visual Studio Code: glob patterns reference,”https://code.
visualstudio.com/docs/editor/glob-patterns.
[30] L. Wachter, J. Gremminger, C. Wressnegger, M. Payer, and F. Toffalini,
“DUMPLING: Fine-grained differential JavaScript engine fuzzing,” in
Network and
Distributed System Security (NDSS) Symposium
, San Diego, CA, USA, 2025.
[31] P. Zhou and Y. Gao, “Detecting prototype pollution for Node.js: Vulnerability review
and new fuzzing inputs,”
Elsevier Computers & Security
, Feb. 2024.
63
