PAY-76Required
Guard every script on a payment page
On every page that shows card fields or the provider's payment iframe, allow only scripts on a written inventory. Detect any change to those scripts or the page's security headers within 7 days, and alert.
How to do it
PCI DSS v4.0.1 requirements 6.4.3 and 11.6.1 have applied since 31 March 2025. Requirement 6.4.3 asks that each script on a payment page is authorised, has its integrity checked and sits on an inventory with a reason. Requirement 11.6.1 asks for a mechanism that alerts on unauthorised changes to the scripts and HTTP headers the browser receives, run at least weekly. Both apply in full under SAQ A-EP and SAQ D. Under SAQ A, an eligibility criterion for pages that embed the provider's iframe replaces the old line items. See Choose a checkout that keeps you on SAQ A says. A plain redirect to a hosted page is outside both. Build the allow list as a Content Security Policy with a report address. Pin fixed third-party files with Subresource Integrity. Then run a scheduled browser job that loads the payment page, lists every script and header, and compares them with the approved list. Card skimming attacks such as Magecart inject one extra script, so the job must fail on any new source.
Examples
JavaScript example
// Playwright: every script on the payment page is on the approved inventory
import inventory from './payment-page-scripts.json' with { type: 'json' }
test('payment page loads only inventoried scripts', async ({ page }) => {
const loaded = []
page.on('request', request => { if (request.resourceType() === 'script') loaded.push(new URL(request.url()).origin + new URL(request.url()).pathname) })
const response = await page.goto('https://example.com/checkout')
expect(loaded.filter(src => !inventory.scripts.includes(src))).toEqual([])
expect(response.headers()['content-security-policy']).toBe(inventory.contentSecurityPolicy)
})
Python example
# Playwright for Python: the same weekly check
import json
from urllib.parse import urlsplit
INVENTORY = json.load(open("payment-page-scripts.json"))
def test_payment_page_loads_only_inventoried_scripts(page):
loaded = []
page.on("request", lambda r: r.resource_type == "script" and loaded.append(
"{0.scheme}://{0.netloc}{0.path}".format(urlsplit(r.url))))
response = page.goto("https://example.com/checkout")
assert [src for src in loaded if src not in INVENTORY["scripts"]] == []
assert response.headers["content-security-policy"] == INVENTORY["contentSecurityPolicy"]
How to check
The repository holds the script inventory with a reason for each entry. A scheduled job ran in the last 7 days and loaded each payment page in a headless browser. It found only inventoried scripts and the approved headers. Adding a test script to the staging payment page makes the next run alert.
Sources
Related rules
- PAY-1 Choose a checkout that keeps you on SAQ A in Payments and billing
- APS-25 Allow only the scripts you wrote in Application security
- APS-33 Lock scripts to a known hash in Application security
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/payments/card-security-and-sca/payment-page-scripts">Agent Wranglers payments and billing playbook</a>. In code you write, add a comment next to it with the same link.