# Your third-party scripts aren't pinned

*A card skimmer hid in a stock analytics script for days. Remote scripts can change under you. Here's why we prefer pinned, reviewable npm packages.*

Engineering | 2026-10-27T17:05:55.703Z

On October 26, [Sansec reported](https://sansec.io/research/convertr-plausible-supply-chain-attack) that a Brazilian Magecart group had added a card skimmer to the Plausible analytics script served from Convertr's own analytics server. Every checkout that loaded that script ran the skimmer. Nobody on those stores changed a line of code.

That's the problem with remote scripts. A `<script src>` tag doesn't pin anything. It runs whatever the server returns today.

## What happened at Convertr

[Convertr](https://sansec.io/research/convertr-plausible-supply-chain-attack) runs online stores and checkout for dozens of Brazilian fashion brands. It self-hosts Plausible Community Edition at analytics.convertr.app.br, and its stores load the tracker from there.

According to [Sansec](https://sansec.io/research/convertr-plausible-supply-chain-attack):

- The attackers appended an obfuscated skimmer to the stock tracker file. The last clean copy Sansec saw was from October 24 at 05:03 UTC. The infected file was detected about 18 hours later.
- The skimmer only runs on Convertr checkout pages. It takes the card number, expiry date, CVV, cardholder name and CPF (Brazil's tax ID), then sends them to an attacker domain.
- On October 26 the attackers updated it to match the field names in Convertr's payment API.
- It was still being served at 15:34 UTC that day. Sansec says it notified Convertr and got no response.

Browsers can cache the script for up to 24 hours. Cleaning the server doesn't clean every visitor's browser right away.

Sansec suspects the way in was CVE-2026-8467, a [remote code execution flaw](https://github.com/advisories/GHSA-55hg-8qxv-qj4p) in a component Plausible CE used. Convertr installed CE 3.0 in 2025 and never updated it. Sansec is clear that it can't confirm the entry point without server logs.

Plausible isn't at fault here. It shipped [v3.2.1](https://github.com/plausible/analytics/releases/tag/v3.2.1) on May 15, 2026, which removed the vulnerable endpoint. The server that served the skimmer was Convertr's, and it hadn't been patched.

## A script tag is a live dependency

This isn't the first time a trusted URL started serving something else. In February 2024 a company bought the cdn.polyfill.io domain and its GitHub account. By June, [Sansec found](https://sansec.io/research/polyfill-supply-chain-attack) that more than 100,000 sites embedding it were serving code that sent mobile users to a betting site. The sites hadn't changed. The owner of the URL had.

Third-party code on payment pages has led to fines in the UK. The ICO [fined Ticketmaster 1.25 million pounds](https://ico.org.uk/media2/migrated/2618609/ticketmaster-uk-limited-mpn.pdf) after malicious code in a chatbot from a vendor, loaded on its payment page, exposed customer data in 2018. The ICO said the risk of third-party scripts on payment pages was known. Ticketmaster [said it would appeal](https://www.bbc.co.uk/news/technology-54931873).

The payments industry now treats this as a requirement. Since March 31, 2025, [PCI DSS requirements 6.4.3 and 11.6.1](https://blog.pcisecuritystandards.org/coffee-with-the-council-podcast-guidance-for-pci-dss-e-commerce-requirements-effective-after-31-march-2025) require payment page scripts to be authorized, checked for integrity and monitored for tampering.

The common thread: a remote script is a dependency with no version number. The vendor can ship a change. So can anyone who controls the vendor's server, domain or account. You find out when someone else notices.

## Packages give you something to check

We think the default should move from script tags to packages, installed from a registry like npm and bundled into your own build.

A package isn't automatically safe. What it gives you is a fixed artifact you can reason about:

- **Pinned.** A committed lockfile records the exact version and integrity hash of every dependency. `npm ci` installs exactly that or fails.
- **Reviewable.** An upgrade is a pull request. You can read the diff before it ships.
- **Auditable.** You can list what's in production and check it against known advisories.
- **Verifiable.** [`npm audit signatures`](https://docs.npmjs.com/verifying-registry-signatures) checks registry signatures and provenance attestations. Packages released with [trusted publishing](https://docs.npmjs.com/trusted-publishers) from public GitHub or GitLab repos get provenance automatically, which links a version to the build that produced it.

The code your visitors run is the code you built and deployed. If it changes, there's a commit.

In this case the option already existed. Plausible publishes an official [tracker package on npm](https://www.npmjs.com/package/@plausible-analytics/tracker).

## npm has its own problems

Moving to npm swaps one trust problem for another. We still think it's the better trade, but you should know what you're signing up for.

Packages get hijacked. In September 2025 an attacker phished a maintainer and published [malicious versions of debug](https://github.com/debug-js/debug/security/advisories/GHSA-4x49-vf9v-38px), [chalk](https://github.com/chalk/chalk/issues/656) and other packages with about [2 billion weekly downloads](https://www.bleepingcomputer.com/news/security/hackers-hijack-npm-packages-with-2-billion-weekly-downloads-in-supply-chain-attack/) combined. The payload targeted crypto transactions in the browser. Two weeks later, [CISA warned](https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem) about Shai-Hulud, a self-replicating worm that spread through more than 500 packages by stealing credentials and republishing.

This isn't new. In 2018 [event-stream](https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident) picked up a malicious dependency from a new maintainer, aimed at a Bitcoin wallet app. In 2021 [ua-parser-js](https://www.cisa.gov/news-events/alerts/2021/10/22/malware-discovered-popular-npm-package-ua-parser-js) shipped three malicious versions.

npm's defaults don't pin either. `npm install` saves dependencies with a [caret range](https://docs.npmjs.com/cli/v11/using-npm/config) like `^1.2.3`, which accepts any later minor or patch release. Without a committed lockfile and `npm ci`, a fresh install can pull a version published an hour ago.

Pinning doesn't stop a bad release. It stops one from reaching you without a decision.

## How to make packages safer

1. Commit your lockfile and install with `npm ci` in CI.
2. Save exact versions with `npm install --save-exact`, or set `save-exact=true` in `.npmrc`.
3. Add a release delay. npm's [`min-release-age`](https://docs.npmjs.com/cli/v11/using-npm/config) (in days) and pnpm's [`minimumReleaseAge`](https://pnpm.io/supply-chain-security) (in minutes, one day by default) skip versions that are too new. The hijacked debug versions were pulled the same day.
4. Read dependency diffs before you merge upgrades, especially for code that runs on checkout pages.
5. Run `npm audit signatures` in CI.
6. Publish your own packages with trusted publishing instead of long-lived tokens.

## If you have to load a remote script

Some vendors only offer a script tag. In that case:

- Self-host a fixed copy where the license allows it.
- Add [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity). The browser blocks the file if its hash doesn't match. The server has to send CORS headers, and it can't work for files that change per request, like polyfill.io's.
- Use a Content Security Policy to limit where scripts load from and where data can go.
- Monitor payment pages for script changes, as PCI DSS 11.6.1 requires.

SRI would have fit Convertr's case. The tracker was a stock file that only changes on upgrade.

Packages wouldn't have saved British Airways in 2018. The attacker [edited a JavaScript file on BA's own site](https://ico.org.uk/media2/migrated/2618421/ba-penalty-20201016.pdf), and the ICO fined BA 20 million pounds. Packages protect what you build. You still have to protect what you deploy, which is where the monitoring above comes in.

## Why we build this way

We ship [c15t](https://github.com/c15t/c15t), our open source consent library, as npm packages. That's part of what we mean by compliance as code: consent runs from versioned code you install, pin and review like any other dependency, not a tag pasted into a page.

Check what your checkout page loads today. Every script tag there is a dependency without a lockfile.

## Keep reading

[All articles](/blog)