Page scripts: what your payment pages run
A card skimmer (Magecart and the like) is a script: added to a checkout through a compromised tag manager, a third-party library, or the site itself, it reads the card as it is typed and sends it elsewhere. Nothing on the server sees it — the page the server sent was clean; the browser ran one more script.
PCI DSS 4.0 asks two things of every page that takes a card, mandatory since March 2025:
- 6.4.3 — every script that runs on the payment page is authorized, its integrity assured, and kept in an inventory with a written justification of why it is needed;
- 11.6.1 — a change- and tamper-detection mechanism alerts personnel to unauthorized changes to the page as the consumer's browser receives it.
Page scripts keeps that inventory from what browsers actually ran, and tells you when something new runs.
Watch your payment pages
In the dashboard, Page scripts → Watched pages: one path a line, exact
(/checkout) or a prefix ending in * (/checkout/*, /pay*). Up to twenty.
Two witnesses report what runs there — use either, or both:
The agent, on a page it is loaded on. Once the page has loaded it lists every
<script>and every script the browser fetched, then whatever is added after, and sends them with its receipt: an external script by its URL (the query dropped — a cache-buster changes on every deploy — except what says whose script it is:id,client-id,sdkid,company_id,keyandrender. Sogtm.js?id=GTM-YOURSand an attacker'sgtm.js?id=GTM-THEIRSare two scripts, and PayPal's SDK is checked with your merchant'sclient-id), an inline one by the hash a Content Security Policy names it by (sha256-…), never its text. A script carried in its owndata:URL is listed by the hash of that URL (data:sha256-…): authorizing one authorizes no other. (A browser's own report says onlydata, so there it isdata:, and ablob:isblob:from either.) One padded past 2,048 characters is listed cut, and a long list goes in several reports. Neither can make the page's list too big to send. JSON and template blocks are data, and are left out. On by default wherever pages are watched;scriptInventory: falseturns it off.Your browser's own report-only policy, with no agent at all. Send this header on the watched pages (the dashboard shows it with your key):
Content-Security-Policy-Report-Only: script-src 'none'; report-uri https://api.antibot.dev/api/v4/csp/pk_live_…It blocks nothing; every script the page runs is reported. Both forms are read. To use the Reporting API as well, add
; report-to antibot-scriptsto the policy and sendReporting-Endpoints: antibot-scripts="https://api.antibot.dev/api/v4/csp/pk_live_…"beside it. Chromium then sends its reports there only, in batches (up to a hundred, read up to 512 KB). Firefox keeps toreport-uri. Behind the edge,ANTIBOT_SCRIPT_PAGES=/checkout/*,/paysets this up for you on those pages, reporting first-party through the edge's own mount (First-party proxy).
Three witnesses, then the inventory
A browser sends whatever its owner wants, and the public key is public. So a script is in the inventory only once three witnesses saw it on the page — three devices by their receipts, or three networks for reports — and nothing about it is written before that. A skimmer reaches every visitor of the page within minutes; one visitor's forgery, or an inline script carrying that visitor's own token, reaches nobody else, and raises no alarm.
Three witnesses are cheap for someone set on it — three receipts, three networks — so what a forger can do is bounded as well:
- a witness counts at most a hundred new scripts an hour (a page has fewer), and a policy report at most fifty; an environment, from all its witnesses together, five thousand an hour, so many networks naming made-up scripts cannot fill the store that also holds the rate limits and velocity counts — five thousand from devices with receipts and five thousand from policy reports, which need only the site's public key, so a flood of reports cannot hold back what the site's visitors' devices name;
- an environment's inventory takes at most twenty new scripts an hour, and
five thousand in all. What is past that is held back and said once an hour
(
script.withheldon the alert endpoints, and the audit log); it enters the next hour on its next sighting, its witnesses still counted; - new scripts are emailed at most once in ten minutes, however many arrive: the dashboard lists them all.
A single-page app is followed: each script goes with the path it was seen on, arriving at a watched page lists it, and leaving one stops looking.
New scripts are told
When a script enters the inventory it is new, and you are told: on the
endpoints that take alerts (script.new), in the
audit log, and by email to the organization's owners and admins when mail is
configured. The email writes each address so no mail client makes a link of
it (hxxps://cdn[.]example[.]com/app[.]js): browsers report the scripts a page
ran, and anyone holding the public key can report one, so the alert's only link
is its own, to the dashboard. A script whose URL changes — a new version's path
— is a new script.
Decide on each in the dashboard: Authorize it with why it is needed (the justification 6.4.3 asks for is required), or Reject it. A rejected script still seen is still listed, with when it was last seen: that is incident response, not something an inventory can stop.
What changes after authorizing
The other half of 11.6.1 is what a browser cannot tell: a file that changes behind the same URL, and the page's own security headers. Once a day:
- every authorized external script is fetched from where browsers fetch
it and its file hashed. A file that differs from the last check puts the
script back to new — authorized as it was, not as it is — and tells you
(
script.changed). One with Subresource Integrity is fetched too: a browser refuses its changed file itself, which breaks the page, and the inventory's word that a script carries SRI is what its browsers said, believed once three of them have — never one report. A script just authorized has its file taken at the next check, within minutes, before any other is due: that file is the one you authorized, and what later checks compare with; - every watched page, at each of your organization's allowed origins
(a prefix pattern at its prefix:
/checkout/*is fetched as/checkout/), is fetched with no cookies and its security headers kept — both content security policies,Strict-Transport-Security,X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy,Cross-Origin-Opener-Policy. Any change is told (page_headers.changed), with before and after, and shown under the page in the dashboard.
Only URLs you authorized, or your own origins, are ever fetched, from public addresses, with no redirects followed and the answer capped. A page that needs a signed-in session to show is checked as an anonymous visitor sees it.
The policy your inventory allows
Once the report-only header has gone quiet — everything that runs is authorized
— the dashboard writes the enforcing policy your inventory allows: each
authorized external script by its URL, each authorized inline script by its
hash, and data:, blob:, 'unsafe-eval' or 'wasm-unsafe-eval' only where
a script of that kind was authorized. A browser's report names a script from
a data: or blob: address by its scheme alone, and the inventory lists it
so. Sent as Content-Security-Policy, it is what stops a script nobody
authorized from running at all; keep its report-uri, and anything it blocks
is still reported here.
What it is not
- It lists what browsers ran, not what a script's file holds: an authorized URL whose content changes is the same script to the inventory. Pin what you can with Subresource Integrity (the inventory shows which scripts arrived with it), or serve it yourself.
- Pages are watched by path. A payment page in a third party's iframe (a hosted card field) is that party's page: it is not in your inventory, and under PCI DSS it is theirs to manage.