Introduction
What if I told that at 2 AM in a Tokyo Hotel Room, instead of watching Netflix we ended up
uncovering a vulnerability on Netflix ? This discussion happened in September 2023, when I met up with Shubs, one of the best bug bounty hunters I’ve ever encountered and the co-founder of Assetnote. We had just wrapped up a HackerOne live hacking event for PayPal, and Shubs had just won the Most Valuable Hacker (MVH) award for the event. We had briefly talked throughout the event, but only really got a chance to get to know each other after the awards ceremony.
Many organizations expose external assets, servers, web applications, and other publicly
accessible resources,as well as internal software dependencies. This dual exposure is vital to understand because attackers can hit from the outside through publicly facing applications, or slip in through unclaimed or vulnerable libraries within the supply chain. From an offensive security perspective, one of those two entry points could be a goldmine, making it insanely complex for defenders to lock down both external entry points and internal dependency vulnerabilities.
Over that late-night chat, we realized a natural synergy between Assetnote’s all-seeing reconnaissance and Depi’s unique offensive security capability to hunt for Software Supply Chain vulnerabilities and prove exploitability. By bridging these two strengths, we uncovered a critical vulnerability in Netflix’s software supply chain. And that’s where this entire research begins.
What Is Dependency Confusion and Why Hunt It ?
In 2021, security researcher Alex Birsan coined the term dependency confusion when he demonstrated that public package repositories could override identically‑named internal packages during automated builds. His proof‑of‑concept shells landed inside CI pipelines at Apple, PayPal, Microsoft, Tesla and dozens of other household names, netting him well over $100 000 in bounties.
The elegance of the attack is its passivity: you register a package with the right name (and usually a higher semantic‑version string) on a public registry such as npm or PyPI. The victim’s build process pulls you in, you never have to touch their perimeter directly. Once the malicious package is installed, it executes with the same permissions as legitimate build steps, making RCE almost trivial.
Because the exploit channels traffic from the victim to you, it is deceptively stealthy. A single outbound HTTP(S) request whoami
, hostname
, pwd
, whatever minimal beacon you choose, is enough to prove code execution. We strongly believe that any ethical proof‑of‑concept should stop there: minimal exfiltration, no heavy payloads, and a built‑in kill‑switch so the package can be disabled the moment the test succeeds. (Expect a separate article on the ethics of offensive supply‑chain testing and why we hold this line.)
Performing Detail-Oriented Reconnaissance
One of the key reasons we have succeeded in discovering numerous supply chain attacks through our collaboration is that we took a detail-oriented approach to reconnaissance. When collecting data at scale, many trade-offs are typically made, usually due to reasons related to speed or cost. To discover dependency confusion at scale and with confidence, these half-measures cannot be taken without significant gaps in visibility.
One of the key factors that we're referring to is the mass collection of network traffic when any URL is loaded. You might wonder why this is a unique proposition compared to typical HTML/JS scraping, or why it helps detect dependency confusion. To explain this, we need to talk a little bit about modern frontend stacks and how they work.
If your dependency confusion detections rely on simple scraping of HTML/JS files or analyzing artifacts like package.json, you're missing out on a large attack surface of potential candidates for dependency confusion.
For modern web applications, when you analyze the page network requests, you will notice that many sites will dynamically load additional JavaScript. This dynamic loading is often what is lost when simply scraping without using a headless browser. These files frequently contain gold, and at times, the entire package list is embedded deep within the JavaScript.
As Assetnote had a large corpus of data for all of these network interactions for every bug bounty asset, we were able to transform this data into something that could help us detect dependency confusion at scale.
How Did We Do the Reconnaissance?
Artefact‑Definition Files vs. Live JavaScript
Initially we considered two reconnaissance strategies:
- Pull artefact definition files (
package.json
,requirements.txt
, etc.) exposed on web roots. Those files are gold when they exist, but they are often hidden behind private repos or build pipelines. - Pull live JavaScript and mine it for imports. This approach covers all modern single‑page apps that split their code into run‑time chunks, exactly where hidden dependencies lurk.
Using both approaches in tandem gave us near‑total coverage of a target’s dependency graph.
What are HAR Files ?
The key to the second approach lies in generating and analysing HAR (HTTP Archive) files that capture HTTP interactions during a web session. HAR files contain valuable information, including the URLs of JavaScript bundles and assets, which often include references to package names and dependencies.
A HAR is essentially a JSON-formatted log the browser records while it loads a page: every request (method, URL, headers, timings, status codes) and every matching response (headers, body size, MIME type) is timestamped and ordered so you can replay the exact waterfall later. You can grab one manually by opening DevTools, switching to the Network tab, ticking Preserve log, reloading the page, and choosing Save all as HAR with content. For fully automated capture, a one-liner with Playwright does the same thing without touching DevTools:
npx playwright open \
--save-har=example.har \
https://example.com
This spins up a temporary Chromium session, records every request/response during the page load, and dumps them straight into example.har
.
Reconnaissance Pipeline
The reconnaissance pipeline focused on turning raw browser traffic into structured, machine‑readable data:
- Generate HAR files: For every target asset, we spin up a headless browser, load the page, and save a complete HTTP Archive (HAR). Each HAR captures every request and response: HTML, JavaScript bundles, API calls, third‑party CDNs.
- Parse with Rust AST tooling: We feed the JavaScript from those bundles into a Rust‑based Abstract Syntax Tree parser. Unlike regex‑hunt "grep for
require("…")
" shortcuts, AST parsing understands the code structure and catches dynamic import patterns, template‑string paths, and obfuscated variable names that simple scraping misses. - Extract candidate package names: From the parsed AST we emit a clean list of potential npm package identifiers.
- Feed into Depi: Depi cross‑references those names against public registries, is the name unclaimed ? Is a newer version number available publicly than internally ? In internal Research Mode, for unclaimed names, Depi can automatically generate a claiming package that pings back once executed.
By chaining these steps we converted an ocean of network noise into a shortlist of high‑confidence dependency‑confusion leads.
While looking at all the data, there was one alert that sounded too good to be true. A confusion on Netflix ?
The Vulnerability & Exploitation: From "that’ll never fire" to RCE inside Netflix
We had already processed ≈ 1 TB of JavaScript when one harmless-looking identifier caught our eye in fast-com.nflximg.net/app-29fb75.js
:
module.exports = logger;
}), require.define("/node_modules/nf-cl-logger/package.json", function(require, module, exports, __dirname, __filename, process, global) {
module.exports = {
main: "index.js"
};
}), require.define("/node_modules/nf-cl-logger/index.js", function(require, module, exports, __dirname, __filename, process, global) {
"use strict";
module.exports = require("./src/logger");
}), require.define("/node_modules/nf-cl-logger/src/logger.js", function(require, module, exports, __dirname, __filename, process, global) {
"use strict";
function createCompactLogger(optionsArg) {
var options = optionsArg || {};
return options.version = options.version || "2.0", options.envelopeName = options.envelopeName || "CompactConsolidatedLoggingEnvelope",
new Logger(options);
}
var Logger = require("./logger-core");
module.exports = createCompactLogger;
}), require.define("/node_modules/nf-cl-logger/src/logger-core.js", function(require, module, exports, __dirname, __filename, process, global) {
"use strict";
function Logger() {
this._init.apply(this, arguments);
}
nf-cl-logger
felt very Netflix-y ("nf" is their ubiquitous prefix), yet Depi flagged it as a Dependency Confusion. Here was my thought process:
Me: "No way their production bundle would ever reach out to public npm.
Me again: "…unless a dev’s laptop or a misconfigured CI job still resolves the name."
Also Me: "Ok maybe I shouldn’t just assume stuff and just see ?"
We shelved it, until curiosity outweighed ego. On 1 Oct 2024, 10:57 UTC we hit publish.
No privilege escalation, no lateral movement, just enough to prove code execution and then exit.
Too much hours of radio silence 🔕️
Nothing happened on day 1. By day 2 at 17:48 UTC on 2 Oct 2024 our dashboard exploded. Ten requests in rapid succession hit our server:
{
"timestamp": 1727891306295,
"x-real-ip": "X.X.X.X",
"user-agent": "yarn/1.22.22 npm/? node/v18.20.4 darwin x64",
"asOrganization": "Netflix Streaming Services",
"asn": 2906
}
My heart spiked, darwin x64 meant a developer laptop or an ephemeral CI runner on macOS, not a stale cache. The IPv6 /48 traced to Netflix’s Santa Clara edge, confirmed by this command:
$ whois X.X.X.X | grep -Ei 'OrgName|descr|origin'
OriginAS: AS2906
OrgName: Netflix Inc
Proof, undeniable, timestamped, and sitting on a Netflix-owned host. We immediately hit the off-switch of our package to avoid any unwanted behaviours and directly sent our report .
The emotional roller-coaster 🎢
The moment the first beacon arrived I tried to call Shubs on Signal because I didn’t know what to do hahaha.
Two minutes later, Shubs just sent the message "nice!" which I imagine it’s Shubs’ way to say "OMG"
"We just dependency-confused Netflix. From our crazy pipeline theory that we discussed at 2 AM in Tokyo."
Why it mattered
-
Confidentiality code execution on a developer box is one command away from internal privilege escalation
-
Integrity poisoned packages can inject subtle logic bombs; ours stopped at a beacon, but real attackers wouldn’t.
-
Availability In theory when we do that kind of Dependency Confusion, we replace a legitimate package by an unwanted one which could crash some CI jobs. It’s more of an integrity issue but ¯\_(ツ)_/¯
In short, a small package push, a forgotten logging helper, and four days of suspense turned into Remote Code Execution inside one of the world’s most security-aware companies.
Why the Relationship Between Security Researchers and Companies is important
The relationship between security researchers and companies is vital for effective vulnerability management. It allows organizations to learn from their weaknesses while enabling researchers to contribute to a safer software ecosystem.
Netflix’s response to our report was exemplary. Their mature bug bounty program ensured timely communication and clarity throughout the triage and resolution process. This level of professionalism creates trust and collaboration between researchers and organizations. We want to thank Netflix for their assessment, transparency, and commitment to improving their security posture.
Conclusion
From a jet-lagged 2 AM brainstorm in Tokyo to a full-blown Remote Code Execution proof on Netflix, this journey shows just how wild the modern supply-chain frontier can be. A single unclaimed logging helper slipped through the cracks and turned into a front-row lesson on why every dependency, public or private, deserves the same scrutiny you’d give production code.
Massive shout-out to Shubs for the midnight spark that glued Assetnote’s panoramic recon to Depi’s exploit factory. Your instinct for "there’s something here, keep digging" turned a maybe into a mic-drop. And thank you to the Netflix security crew for the lightning-fast triage, crystal-clear comms, and the kind of professionalism that makes responsible disclosure worth every late night. You patched the hole, shared the lessons, and helped raise the bar for everyone.
Here’s to more caffeine-fuelled hotel-room epiphanies, and to a safer, saner software ecosystem for all of us. 🥂
We’ll be back soon.