pci-payment-ui-security-review
Statically review payment-page frontend code for PCI-DSS-relevant defects in the browser/DOM slice only — raw PAN collection in self-controlled inputs instead of Stripe hosted fields, card data persisted client-side or to analytics, raw card data POSTed to a first-party endpoint,
Install
npx skills add https://github.com/VincentChuWaiChow/vanguard-frontier-agentic/tree/master/skills/frontend/pci-payment-ui-security-review
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install vincentchuwaichow-vanguard-frontier-agentic@llmmart
git clone https://github.com/VincentChuWaiChow/vanguard-frontier-agentic.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole vincentchuwaichow/vanguard-frontier-agentic collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
PCI Payment UI Security Review
Purpose
Review payment-collection frontend code — checkout pages, card-entry forms, and any script loaded on a page that collects cardholder data — for the frontend-specific defect classes that keep raw cardholder data out of the DOM and out of merchant-controlled JavaScript: raw PAN/CVV collection in a self-controlled <input> instead of Stripe's iframe-isolated hosted fields, card data persistence to localStorage/sessionStorage/analytics/logs, raw card data POSTed to a first-party endpoint instead of tokenized first, and third-party scripts loaded on the payment page without Subresource Integrity (SRI). This skill exists so the review stays anchored to the frontend tokenization boundary and script-integrity surface instead of drifting into a general "payments code review."
When to use
Use this skill when the user asks to:
- review a checkout page or card-entry form for PCI-relevant frontend risk,
- assess whether card-number, expiry, or CVV fields are collected safely (hosted fields vs. raw
<input>), - audit which scripts load on a payment-collection page and whether they carry Subresource Integrity,
- assess SAQ-A scope reduction claims for a page that outsources card collection to Stripe Elements/Payment Element,
- perform a pre-launch frontend security review of a Stripe-based payment integration.
Do not use this skill for:
- a full PCI-DSS compliance audit — this skill covers only the frontend slice (client-side script inventory/integrity, hosted-field isolation, tokenization boundary). Server-side cardholder-data-environment (CDE) segmentation, network firewalling, key management, encryption-at-rest, access control, and the remaining PCI-DSS requirement families are out of scope; route those to a dedicated infrastructure/compliance review,
- a non-payment page with no card-data collection or third-party script surface — general frontend security review is a different scope,
- a bug that requires live traffic capture (a proxy/network trace confirming what actually left the browser) to confirm exploitation — static analysis proves the structural risk (the code path that would expose or persist raw PAN/CVV), not that raw card data has already left the browser in production.
Context7 Documentation Protocol
- Resolve the Stripe library ID with
resolve-library-idbefore citing any Stripe Elements/Payment Element or tokenization-mechanism claim; usequery-docsto corroborate the specific API behavior (e.g.,stripe.createToken(cardElement),stripe.confirmPayment(),stripe.confirmCardPayment()) rather than relying on memory. - Label every Stripe-API behavioral claim
documentation-based(orrepo evidenceif confirmed directly against fetched Context7 doc content) — do not assert current Stripe API shape from training memory alone. - Label every PCI-DSS requirement claim (6.4.3, 11.6.1, or the general SAQ-A scope-reduction rationale)
standard-based— this skill does not verify a merchant's actual PCI compliance status or attestation; it evaluates whether the frontend code matches the documented pattern the standard and Stripe's architecture rely on. - If Context7 is unavailable, fall back to the
official_docsURLs in this skill'smetadata.jsonand label the claimdocumentation-based, unverified against current release. - Read
package.json/script tags first to confirm which Stripe integration is in use (@stripe/stripe-js+ Elements,Payment Element, or a legacy/custom integration) — the safe idiom and API names differ; do not assume Elements is in use without confirming.
Lean operating rules
- Raw PAN/CVV collection outside an iframe-isolated hosted field, and any client-side persistence of raw cardholder data, default to HIGH severity. This is a security-scoped skill: do not downgrade a raw-PAN-in-DOM finding to MEDIUM because "it's only in a dev/staging build" — the structural pattern is the risk, not its current deployment target.
- Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this form might expose card data" without showing the specific
<input>element, the specific.valueread, or the specific POST body construction is not a valid finding — it is a guess. - Do not flag
<CardNumberElement />,<CardExpiryElement />,<CardCVCElement />, or<PaymentElement />usage as a defect — these are Stripe's iframe-rendered hosted fields; the PAN/CVV they collect stays inside iframes controlled by Stripe and is never accessible to merchant JavaScript. Only a self-controlled<input>(e.g.,<input type="text" id="cardnumber" />) that application JS reads via.valueis the raw-pan-input defect. - Treat any
localStorage,sessionStorage, in-memory app store, or analytics/logging call that could carry a PAN, CVV, or full cardholder data payload as a HIGH finding regardless of whether the value is currently populated in the traced code path — check the shape of the object being persisted, not just its current runtime value. - Treat a first-party POST (
fetch/XMLHttpRequest/form submit to a first-party endpoint) carrying raw card fields (PAN, CVV, expiry) as a HIGH finding. The safe pattern tokenizes first via the Stripe API (stripe.createToken(cardElement)or the Payment Element confirmation flow) and POSTs only the resulting token or PaymentIntent/PaymentMethod ID to the first-party endpoint. - Treat any third-party
<script src="...">loaded on a payment-collection page without anintegrityattribute (Subresource Integrity, e.g. asha256-hash) as a finding — cite PCI-DSS v4 requirement 6.4.3 (script inventory, script authorization, and integrity control) as the standard-based grounding. Do not accept "we trust this vendor" as a substitute for a visible integrity attribute or equivalent cryptographic verification. - Stripe's hosted fields (
CardNumberElement,CardExpiryElement,CardCVCElement,PaymentElement) render inside sandboxed iframes controlled by Stripe, not merchant code — the browser's same-origin policy and the iframe sandbox boundary are what keep merchant JavaScript from ever reading raw PAN/CVV values. A missing-iframe-isolation finding applies when card fields are collected via manual<input>elements with no sandboxed hosted-field equivalent in use. - When reviewing broader payment-page change-detection posture, note PCI-DSS v4 requirement 11.6.1 (automated change-detection/alerting for unauthorized script modification on payment pages) as a standard-based expectation — but do not fabricate a finding about a change-detection mechanism you cannot observe in the frontend code under review; note it as an open question instead.
- Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only).
- Load only the reference needed for the concern in scope.
References
Load these only when needed:
- Review workflow and findings contract — use for the step-by-step review procedure, the defect decision tree, and the required output shape.
- Hosted fields and tokenization boundary — load when reviewing card-entry form markup,
raw-pan-input,card-data-persistence, orself-posted-card-datadefect classes. - Script integrity and SAQ-A scope reduction — load when reviewing third-party script tags for Subresource Integrity, or assessing whether a page's use of hosted fields plausibly supports SAQ-A scope-reduction claims.
Response minimum
Return, at minimum:
- the payment-collection page(s), card-entry form markup, script tags, and/or client-side persistence/network calls in scope,
- ranked findings with file:line evidence, defect category (
raw-pan-input,card-data-persistence,self-posted-card-data,unsigned-third-party-script, ormissing-iframe-isolation), the concrete data-flow trace (the raw<input>/.valueread, thelocalStorage/analytics call, the POST body construction, or the missingintegrityattribute), and a fix sketch matching Stripe's documented idiom (CardNumberElement,PaymentElement,stripe.createToken, or anintegrity/SRI attribute), - for every finding involving PAN/CVV/cardholder data exposure, an explicit statement of whether the value stays inside a Stripe-controlled iframe (safe) or is reachable by merchant JavaScript (unsafe) — never approve on the assumption isolation exists without tracing it,
- evidence level per finding (
repo evidence,documentation-based,standard-based, orinference), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited or confirmed non-compliant, - verdict (approve / approve-with-notes / block),
- open questions or scope the review could not cover — explicitly restate that this is not a full PCI-DSS audit: server-side cardholder-data-environment segmentation, key management, network controls, and non-script requirement families are out of scope and require a dedicated compliance/infrastructure review.
Files (vanguard-frontier-agentic)
-
references
-
hosted-fields-and-tokenization.md 7.7 KB
# Hosted Fields and the Tokenization Boundary Use this reference when reviewing card-entry form markup, or investigating the `raw-pan-input`, `card-data-persistence`, or `self-posted-card-data` defect classes. ## What people get wrong The naive assumption is: > "As long as I send the card data over HTTPS to my own server, it's protected in transit, so a plain `<input>` for the card number is fine." Wrong for PCI-DSS purposes. Transport encryption protects data in transit between the browser and the first-party server, but it does nothing to prevent the merchant's own JavaScript — or a compromised/malicious third-party script sharing the page — from reading the raw PAN/CVV the instant it sits in a DOM `<input>` or in memory as a plain JS value. The entire architectural point of Stripe's hosted fields is to remove that reachability, not merely to encrypt what is already reachable. ## Officially grounded tokenization flow (Context7 Stripe documentation) - `stripe.createToken(cardElement)` converts data collected by Stripe Elements into a **single-use token**, passed securely to the server — the merchant's own code never handles the raw values that produced the token. - Raw card data (PAN, CVV) collected by `CardNumberElement`, `CardExpiryElement`, and `CardCVCElement` stays **inside iframes controlled by Stripe**, never accessible to merchant JavaScript. - `stripe.confirmPayment()` and `stripe.confirmCardPayment()` tokenize directly as part of confirming the PaymentIntent, without ever exposing raw values to the page. - The merchant receives only the token, or the PaymentIntent/PaymentMethod confirmation result — never the raw card data itself. - **Key architectural guarantee:** Elements render inside sandboxed iframes; merchant code never sees or touches the PAN or CVV. The browser's same-origin policy prevents scripts on the merchant page — including the merchant's own code and any co-located third-party script — from reading iframe content. ## Defect class 1: `raw-pan-input` Raw PAN collection in a self-controlled `<input>` element. - **Dangerous:** `<input type="text" id="cardnumber" />` with application JS reading `.value` to obtain the card number. The moment card-number digits exist as a plain string in application-reachable memory or the DOM, they are reachable by merchant JS, any injected script, and any browser extension with page access. - **Safe:** `<CardNumberElement />` (iframe-rendered by Stripe; no app JS access to the value inside). - **Verification targets:** Grep for `<input` elements with `type="text"`/`type="number"`/`name`/`id` attributes suggestive of card fields (`card`, `pan`, `cardnumber`, `ccnum`), and confirm whether the surrounding component imports and uses `CardNumberElement`/`PaymentElement` instead, or reads `.value` directly from a raw input. ## Defect class 2: `missing-iframe-isolation` No iframe/hosted-field isolation for card-data collection generally (PAN, CVV, *or* expiry). - **Dangerous:** Manual `<input>` fields for PAN, CVV, and/or expiry with no iframe sandboxing — a homegrown card form. - **Safe:** `<CardNumberElement />`, `<CardExpiryElement />`, `<CardCVCElement />`, or the unified `<PaymentElement />`, all of which render inside a sandboxed iframe boundary that the merchant page cannot reach into. - This is the broader structural finding that `raw-pan-input` is one specific instance of — flag it whenever *any* of the three card fields (not just the PAN) is collected outside a hosted field. ## Defect class 3: `card-data-persistence` Card data persisted to a client-side store, `localStorage`/`sessionStorage`, analytics, or logs. - **Dangerous:** `localStorage.setItem('card', {pan, cvv, exp})`, a Vuex/Redux/Pinia `store` action that commits raw card fields to persisted state, or an analytics event (`analytics.track('checkout_attempt', {cardNumber, cvv})`) that includes cardholder data fields. - **Safe:** Persist only the tokenized `paymentMethod` ID (e.g., `store.commit('setPaymentMethod', paymentMethod.id)`) — never raw PAN, CVV, or full cardholder data. - **Verification targets:** Grep for `localStorage.setItem`/`sessionStorage.setItem` calls, and for `store`/state-management writes and analytics/logging calls, in files reachable from the payment form. For each match, inspect the literal object shape being persisted for PAN- or CVV-shaped fields, even under generic key names — a field literally named `PAN` or `CVV`, or one holding a 13–19 digit numeric string or 3–4 digit CVV pattern, is the signal to trace, regardless of the variable name chosen. ## Defect class 4: `self-posted-card-data` Raw card fields POSTed to a first-party endpoint. - **Dangerous:** `fetch('/api/pay', {body: {pan, cvv, exp}})` — raw card fields constructed directly into a POST body and sent to the merchant's own `/api/pay` first-party endpoint, with no tokenization step beforehand. - **Safe:** `stripe.createToken(cardElement).then(token => fetch('/api/pay', {body: {token: token.id}}))` — the Stripe API tokenizes first; only the resulting token is POSTed to the first-party endpoint. - **Verification targets:** Grep for `fetch(`/`XMLHttpRequest`/form `action=` submissions targeting a first-party API path from payment-form components. For each, trace the request body's construction backward: does it reference raw card fields (`pan`, `cardNumber`, `cvv`, `exp`), or does it reference a token/PaymentIntent/PaymentMethod object produced by a preceding Stripe API call? ## Minimal safe implementation pattern ```jsx // Safe: hosted fields + tokenize-before-POST import { CardNumberElement, CardExpiryElement, CardCVCElement, useStripe, useElements } from '@stripe/react-stripe-js' function CheckoutForm() { const stripe = useStripe() const elements = useElements() async function handleSubmit(e) { e.preventDefault() const cardElement = elements.getElement(CardNumberElement) // Raw PAN/CVV never leave the Stripe iframe; only a token is produced. const { token } = await stripe.createToken(cardElement) // Only the token is persisted or sent to the first-party endpoint. await fetch('/api/pay', { method: 'POST', body: JSON.stringify({ token: token.id }) }) } return ( <form onSubmit={handleSubmit}> <CardNumberElement /> <CardExpiryElement /> <CardCVCElement /> </form> ) } ``` Anti-pattern (do not approve): ```html <!-- WRONG: raw PAN/CVV in a self-controlled input, read directly by app JS --> <input type="text" id="cardnumber" /> <input type="text" id="cvv" /> <script> function submitPayment() { const pan = document.getElementById('cardnumber').value const cvv = document.getElementById('cvv').value localStorage.setItem('lastCard', JSON.stringify({ pan, cvv })) // card-data-persistence fetch('/api/pay', { method: 'POST', body: JSON.stringify({ pan, cvv }) }) // self-posted-card-data } </script> ``` ## Adversarial checklist Before clearing a payment form as safe on the tokenization boundary, answer these: - Is card number, expiry, and CVV collection fully delegated to `CardNumberElement`/`CardExpiryElement`/`CardCVCElement`/`PaymentElement`, or does any field use a manual `<input>`? - Does any code path read `.value` from a manual card-shaped input? - Does any `localStorage`/`sessionStorage`/store/analytics/log call persist an object containing PAN-, CVV-, or cardholder-data-shaped fields? - Does the first-party POST body reference raw card fields, or does it reference only a Stripe token/PaymentIntent/PaymentMethod ID produced by a preceding `stripe.createToken`/`stripe.confirmPayment`/`stripe.confirmCardPayment` call? If any answer reveals raw PAN/CVV reachable by merchant JS at rest, in a POST body, or in persisted state, the finding is HIGH and structural — report it even without a reproduced data-exfiltration incident. -
script-integrity-and-scope.md 6.2 KB
# Script Integrity and SAQ-A Scope Reduction Use this reference when reviewing third-party script tags loaded on a payment-collection page for Subresource Integrity, or when assessing whether a page's architecture plausibly supports an SAQ-A scope-reduction claim. ## What people get wrong The naive assumption is: > "This third-party script is from a well-known vendor's CDN over HTTPS, so it's safe to load on the checkout page." Wrong for PCI-DSS purposes. HTTPS protects the script in transit from the CDN to the browser; it does not protect against the CDN itself being compromised, the vendor's build pipeline being compromised, or a supply-chain attack that swaps the script's content while its URL stays identical (the class of attack behind real-world Magecart-style card-skimming incidents). Subresource Integrity closes exactly this gap: it lets the browser refuse to execute a script whose fetched content does not match a cryptographic hash pinned at authorization time. ## PCI-DSS v4 grounding (standard-based) **6.4.3 — Script Inventory, Authorization & Integrity Control:** - All scripts loaded on payment collection pages must be inventoried and approved. - Scripts must be authorized before loading (script authorization). - Integrity verification is required via Subresource Integrity (SRI) or an equivalent cryptographic verification mechanism (e.g., a SHA-256 hash). **11.6.1 — Change Detection on Payment Pages:** - Automated change-detection/alerting mechanisms are required for unauthorized code additions or modifications to payment pages. - This is intended to detect malicious script injections, DOM tampering, and compromise attempts in near-real time, independent of the SRI control above. Both of these are standard requirement citations (`standard-based`) — this skill can confirm from frontend code whether SRI is present on a given script tag, but it cannot confirm from static frontend review alone whether a merchant has an operating script-inventory/authorization *process* or a functioning 11.6.1 change-detection deployment. Note any such gap as an open question rather than asserting non-compliance. ## Defect class: `unsigned-third-party-script` Third-party script loaded without Subresource Integrity. - **Dangerous:** ```html <script src="https://cdn.example.com/analytics.js"></script> ``` No `integrity` attribute means the browser will execute whatever content the CDN serves at request time, with no cryptographic check against what was reviewed/authorized. - **Safe:** ```html <script src="https://cdn.example.com/analytics.js" integrity="sha256-C6CB9UYIS9UJeqinPHWTHVqh/E1uhG5Twh76tviuROE=" crossorigin="anonymous"></script> ``` The `integrity` attribute pins a SHA-256 (or SHA-384/SHA-512) hash; the browser refuses to execute the script if the fetched bytes don't match. `crossorigin="anonymous"` is required alongside `integrity` for cross-origin script resources so the browser can perform the integrity check. - **Verification targets:** Grep every `<script src="...">` tag rendered on a payment-collection page (including scripts injected via a tag-manager snippet, if visible in source) for the presence of an `integrity` attribute. A same-origin/first-party script (served from the merchant's own domain) is lower risk but still benefits from SRI if served via a CDN in front of first-party assets; the primary finding target is third-party origins. - A missing `integrity` attribute on a third-party script on a payment page is a HIGH finding regardless of the vendor's reputation — reputation is not a substitute for script authorization and integrity verification under 6.4.3. ## SAQ-A scope reduction (standard-based) SAQ-A is the PCI-DSS self-assessment questionnaire for merchants that have **fully outsourced** all cardholder data collection and processing to a PCI-validated third party (e.g., Stripe), such that the merchant's own systems never store, process, or transmit cardholder data and never touch it even transiently in the browser. For a payment page's frontend architecture to plausibly support an SAQ-A claim: - Every card field (PAN, expiry, CVV) must be collected exclusively via Stripe hosted fields (`CardNumberElement`/`CardExpiryElement`/`CardCVCElement`) or the unified `PaymentElement` — no manual `<input>` anywhere in the flow (would otherwise be a `raw-pan-input`/`missing-iframe-isolation` finding). - No client-side persistence of raw cardholder data anywhere in the flow (would otherwise be a `card-data-persistence` finding). - No first-party POST of raw card fields anywhere in the flow — only tokens/PaymentIntent/PaymentMethod IDs cross to the first-party server (would otherwise be a `self-posted-card-data` finding). This skill can confirm the frontend evidence for or against these three conditions. It **cannot** confirm the merchant's actual SAQ-A eligibility determination or attestation — that is a compliance/assessor decision that also depends on the merchant's payment page hosting model (e.g., whether the payment page itself is served from PCI-relevant infrastructure) and other SAQ-A eligibility criteria outside this skill's frontend-only scope. Label any SAQ-A-adjacent conclusion `standard-based` and explicitly note the full-audit exclusion. ## Adversarial checklist Before clearing a payment page's script surface: - Does every third-party `<script src="...">` on the page carry an `integrity` attribute with a `sha256-` (or stronger) hash and a `crossorigin` attribute? - Is there any script loaded dynamically (via `document.createElement('script')` or a tag-manager container) that bypasses static `integrity` attribution entirely? Flag this as a distinct, harder-to-verify risk — dynamically injected scripts cannot carry a static `integrity` attribute in the same way, and the review should note this as an open question requiring the tag-manager's own vendor controls to be checked separately. - If asked about SAQ-A: do all three frontend conditions above hold, with no raw-PAN, persistence, or self-posted-data findings anywhere in the reviewed flow? - Is there any observable client-side change-detection or integrity-monitoring code on the page (relevant to 11.6.1), or is this an open question requiring server/ops-side confirmation? -
workflow-and-output.md 8.1 KB
# Review Workflow and Findings Contract Use this reference for the step-by-step review procedure and the required output shape. Load the other two references only for the specific defect class the payment page under review actually raises. ## Prerequisites - Confirm the page under review actually collects or handles cardholder data: a checkout form, a saved-card management page, or any page that loads Stripe.js or renders card-entry fields. If no such page exists in scope, this skill does not apply. - Identify the Stripe integration shape in use: `@stripe/stripe-js` + `Elements` (`CardNumberElement`/`CardExpiryElement`/`CardCVCElement`), the unified `Payment Element`, or a legacy/custom card-collection integration. The safe idiom and the specific API calls to verify differ by shape. - Remember the scope boundary throughout: this is a **frontend-only** PCI-DSS review. Do not extend findings into server-side cardholder-data-environment segmentation, network firewalling, key management, or encryption-at-rest — those require a dedicated infrastructure/compliance review and are out of scope here. ## Workflow 1. **Locate every card-entry surface.** For each checkout/payment form, identify whether card number, expiry, and CVV are collected via Stripe hosted fields (`CardNumberElement`, `CardExpiryElement`, `CardCVCElement`, or `PaymentElement`) or via manual `<input>` elements. See `references/hosted-fields-and-tokenization.md` for the `raw-pan-input` and `missing-iframe-isolation` decision tree. 2. **Trace client-side persistence and analytics calls.** Grep for `localStorage.setItem`, `sessionStorage.setItem`, in-memory app `store` writes, and analytics/logging calls (`analytics.track`, `console.log`, error-reporting SDK calls) reachable from the payment form's state. For each, inspect the shape of the persisted/logged object for PAN, CVV, or other cardholder data fields. See `references/hosted-fields-and-tokenization.md` for the `card-data-persistence` decision tree. 3. **Trace network calls from the payment form.** For each `fetch`/`XMLHttpRequest`/form submission triggered by the payment form, determine whether raw card fields (PAN, CVV, expiry) are POSTed directly to a first-party endpoint, or whether a Stripe tokenization call (`stripe.createToken(cardElement)`, `stripe.confirmPayment()`, `stripe.confirmCardPayment()`) runs first and only the resulting token/PaymentIntent/PaymentMethod ID is POSTed. See `references/hosted-fields-and-tokenization.md` for the `self-posted-card-data` decision tree. 4. **Enumerate every script tag loaded on the payment page.** For each third-party `<script src="...">`, check for an `integrity` attribute (Subresource Integrity) and a `crossorigin` attribute. See `references/script-integrity-and-scope.md` for the `unsigned-third-party-script` decision tree. 5. **Assess SAQ-A scope-reduction plausibility (if asked).** If the user asks about SAQ-A eligibility, check whether card data collection is fully outsourced to Stripe hosted fields/Payment Element with no raw card data ever touching the merchant's own JavaScript or servers. See `references/script-integrity-and-scope.md`. 6. **Produce ranked findings** using the output contract below. ## Decision tree - Card number/expiry/CVV collected via a manual `<input>` element (e.g., `<input type="text" id="cardnumber" />`) with application JS reading `.value` → **HIGH** finding, `raw-pan-input` and `missing-iframe-isolation`. The safe pattern is `<CardNumberElement />`/`<PaymentElement />`. - Card number/expiry/CVV collected via `<CardNumberElement />`, `<CardExpiryElement />`, `<CardCVCElement />`, or `<PaymentElement />` → not a finding; these are Stripe's iframe-rendered hosted fields, and the browser's same-origin policy prevents merchant JS from reading iframe content. - A `localStorage`/`sessionStorage`/store/analytics call persists an object containing PAN, CVV, or full card data (even if named generically, e.g. `paymentInfo`) → **HIGH** finding, `card-data-persistence`. The safe pattern persists only a tokenized `paymentMethod` ID. - A `localStorage`/`sessionStorage`/store/analytics call persists only a Stripe token, PaymentIntent ID, or PaymentMethod ID (no raw card fields) → not a finding. - A first-party POST body is constructed from raw card fields (`pan`, `cardNumber`, `cvv`, `exp`) without a preceding Stripe tokenization call → **HIGH** finding, `self-posted-card-data`. - A first-party POST body is constructed from a Stripe token (`token.id`) or PaymentIntent/PaymentMethod confirmation result, with the tokenization call (`stripe.createToken`, `stripe.confirmPayment`, `stripe.confirmCardPayment`) preceding it on the same path → not a finding. - A third-party `<script src="...">` on the payment page has no `integrity` attribute → **HIGH** finding, `unsigned-third-party-script` (PCI-DSS v4 6.4.3, standard-based). - A third-party `<script src="..." integrity="sha256-..." crossorigin="anonymous">` on the payment page → not a finding. - User asks about SAQ-A scope reduction and card collection is fully outsourced to Stripe hosted fields/Payment Element with no raw card data touching merchant JS or servers → plausible SAQ-A alignment, label `standard-based`, and note this skill does not verify the merchant's full SAQ-A questionnaire or attestation status. - User asks about SAQ-A scope reduction and any `raw-pan-input`, `card-data-persistence`, or `self-posted-card-data` finding is present → SAQ-A eligibility is not plausible on the frontend evidence reviewed; flag the specific defect blocking it. ## Output contract Every response from this skill must return: 1. **Scope** — the payment-collection page(s), card-entry form markup, script tags, and/or client-side persistence/network calls reviewed. 2. **Ranked findings** — each with file:line, defect category (`raw-pan-input` / `card-data-persistence` / `self-posted-card-data` / `unsigned-third-party-script` / `missing-iframe-isolation`), the concrete data-flow trace (the raw input read, the persistence/analytics call, the POST body construction, or the missing integrity attribute), and a fix sketch matching Stripe's documented pattern. 3. **Iframe-isolation status per PAN/CVV-handling finding** — an explicit statement of whether the value stays inside a Stripe-controlled iframe (safe) or is reachable by merchant JavaScript (unsafe); never infer isolation exists without tracing it. 4. **Evidence level per finding** — `repo evidence`, `documentation-based`, `standard-based`, or `inference`. Label structural risk findings as structural risk — do not imply confirmed exploitation or confirmed non-compliance without live evidence (e.g., a captured network trace, an actual PCI assessor finding). 5. **Verdict** — approve / approve-with-notes / block. 6. **Open questions or out-of-scope items** — always restate explicitly that this is not a full PCI-DSS audit: server-side CDE segmentation, network controls, key management, and non-script requirement families are out of scope. ## When to push back Push back if the user asks to: - approve a raw `<input>` card field because "we sanitize/mask it before sending" — masking display does not change that raw PAN/CVV briefly exists in merchant-readable DOM/JS state; only Stripe's iframe-isolated hosted fields keep it out of merchant JS entirely, - treat a third-party script as safe because "it's from a reputable vendor" without a visible `integrity` attribute — vendor reputation is not equivalent cryptographic verification under PCI-DSS v4 6.4.3, - skip the client-side persistence check because "we only store it in memory, not localStorage" — an in-memory app `store` holding raw PAN/CVV is still a `card-data-persistence` finding if it is reachable by other application code, logging, or a crash-reporting SDK, - call a page "SAQ-A eligible" solely because it uses Stripe.js somewhere on the page — SAQ-A eligibility requires that raw card data never reaches merchant-controlled JS or servers at all; a page that uses Stripe.js for one flow but also POSTs raw card fields elsewhere does not qualify, - treat this review as a full PCI-DSS compliance sign-off — this skill covers only the frontend script/tokenization slice; state the full-audit exclusion explicitly in every response.
-
-
metadata.json 2 KB
{ "id": "pci-payment-ui-security-review", "name": "PCI Payment UI Security Review", "type": "skill", "provider": "frontend", "harnesses": [ "claude-code", "cursor", "codex", "gemini", "kiro", "other" ], "summary": "Reviews payment-collection frontend code for the PCI-DSS-relevant frontend defect classes: raw PAN/CVV collection in self-controlled inputs instead of Stripe hosted fields, card data persisted client-side or to analytics, raw card data POSTed to a first-party endpoint before tokenization, and third-party scripts loaded without Subresource Integrity — grounded via Context7 Stripe documentation and PCI-DSS v4 script-security requirements (standard-based).", "source_type": "original", "official_docs": [ "https://docs.stripe.com/js/elements_object/create", "https://docs.stripe.com/security/guide#validating-pci-compliance", "https://www.pcisecuritystandards.org/document_library/", "https://www.pcisecuritystandards.org/documents/SAQ-A-r2-v4_0.pdf" ], "security_notes": "This skill's entire scope is security-critical: raw PAN/CVV collection outside a Stripe-controlled iframe, client-side persistence of cardholder data, and unsigned third-party scripts on payment pages are all cardholder-data-exposure or code-tampering vectors. Every finding in this skill defaults to HIGH severity unless proven otherwise with concrete evidence of iframe isolation, tokenization-before-POST, or a visible integrity attribute. This is the FRONTEND slice of PCI-DSS only — it is NOT a full PCI-DSS audit; server-side cardholder-data-environment segmentation, network controls, and key management are explicitly out of scope. Static-review-only skill: it reads and greps payment-page source but never executes, builds, or runs application code, and never sends live requests.", "last_verified": "2026-07-03", "path": "skills/frontend/pci-payment-ui-security-review", "author": "github: VincentChuWaiChow", "version": "0.1.0" } -
SKILL.md 9.6 KB
--- name: pci-payment-ui-security-review description: Statically review payment-page frontend code for PCI-DSS-relevant defects in the browser/DOM slice only — raw PAN collection in self-controlled inputs instead of Stripe hosted fields, card data persisted client-side or to analytics, raw card data POSTed to a first-party endpoint, and third-party scripts loaded without Subresource Integrity — grounded in Stripe's own tokenization docs and PCI-DSS v4 script-security requirements. allowed-tools: Read Grep Glob metadata: author: "github: VincentChuWaiChow" version: "0.1.0" updated: "2026-07-03" category: security --- # PCI Payment UI Security Review ## Purpose Review payment-collection frontend code — checkout pages, card-entry forms, and any script loaded on a page that collects cardholder data — for the frontend-specific defect classes that keep raw cardholder data out of the DOM and out of merchant-controlled JavaScript: raw PAN/CVV collection in a self-controlled `<input>` instead of Stripe's iframe-isolated hosted fields, card data persistence to `localStorage`/`sessionStorage`/analytics/logs, raw card data POSTed to a first-party endpoint instead of tokenized first, and third-party scripts loaded on the payment page without Subresource Integrity (SRI). This skill exists so the review stays anchored to the frontend tokenization boundary and script-integrity surface instead of drifting into a general "payments code review." ## When to use Use this skill when the user asks to: - review a checkout page or card-entry form for PCI-relevant frontend risk, - assess whether card-number, expiry, or CVV fields are collected safely (hosted fields vs. raw `<input>`), - audit which scripts load on a payment-collection page and whether they carry Subresource Integrity, - assess SAQ-A scope reduction claims for a page that outsources card collection to Stripe Elements/Payment Element, - perform a pre-launch frontend security review of a Stripe-based payment integration. Do not use this skill for: - a full PCI-DSS compliance audit — this skill covers **only the frontend slice** (client-side script inventory/integrity, hosted-field isolation, tokenization boundary). Server-side cardholder-data-environment (CDE) segmentation, network firewalling, key management, encryption-at-rest, access control, and the remaining PCI-DSS requirement families are out of scope; route those to a dedicated infrastructure/compliance review, - a non-payment page with no card-data collection or third-party script surface — general frontend security review is a different scope, - a bug that requires live traffic capture (a proxy/network trace confirming what actually left the browser) to confirm exploitation — static analysis proves the structural risk (the code path that *would* expose or persist raw PAN/CVV), not that raw card data has already left the browser in production. ## Context7 Documentation Protocol - Resolve the Stripe library ID with `resolve-library-id` before citing any Stripe Elements/Payment Element or tokenization-mechanism claim; use `query-docs` to corroborate the specific API behavior (e.g., `stripe.createToken(cardElement)`, `stripe.confirmPayment()`, `stripe.confirmCardPayment()`) rather than relying on memory. - Label every Stripe-API behavioral claim `documentation-based` (or `repo evidence` if confirmed directly against fetched Context7 doc content) — do not assert current Stripe API shape from training memory alone. - Label every PCI-DSS requirement claim (6.4.3, 11.6.1, or the general SAQ-A scope-reduction rationale) `standard-based` — this skill does not verify a merchant's actual PCI compliance status or attestation; it evaluates whether the frontend code matches the documented pattern the standard and Stripe's architecture rely on. - If Context7 is unavailable, fall back to the `official_docs` URLs in this skill's `metadata.json` and label the claim `documentation-based, unverified against current release`. - Read `package.json`/script tags first to confirm which Stripe integration is in use (`@stripe/stripe-js` + Elements, `Payment Element`, or a legacy/custom integration) — the safe idiom and API names differ; do not assume Elements is in use without confirming. ## Lean operating rules - Raw PAN/CVV collection outside an iframe-isolated hosted field, and any client-side persistence of raw cardholder data, default to HIGH severity. This is a security-scoped skill: do not downgrade a raw-PAN-in-DOM finding to MEDIUM because "it's only in a dev/staging build" — the structural pattern is the risk, not its current deployment target. - Trace every finding to a concrete file:line and a concrete data-flow path. A finding that says "this form might expose card data" without showing the specific `<input>` element, the specific `.value` read, or the specific POST body construction is not a valid finding — it is a guess. - Do not flag `<CardNumberElement />`, `<CardExpiryElement />`, `<CardCVCElement />`, or `<PaymentElement />` usage as a defect — these are Stripe's iframe-rendered hosted fields; the PAN/CVV they collect stays inside iframes controlled by Stripe and is never accessible to merchant JavaScript. Only a self-controlled `<input>` (e.g., `<input type="text" id="cardnumber" />`) that application JS reads via `.value` is the raw-pan-input defect. - Treat any `localStorage`, `sessionStorage`, in-memory app store, or analytics/logging call that could carry a PAN, CVV, or full cardholder data payload as a HIGH finding regardless of whether the value is currently populated in the traced code path — check the shape of the object being persisted, not just its current runtime value. - Treat a first-party POST (`fetch`/`XMLHttpRequest`/form submit to a first-party endpoint) carrying raw card fields (PAN, CVV, expiry) as a HIGH finding. The safe pattern tokenizes first via the Stripe API (`stripe.createToken(cardElement)` or the Payment Element confirmation flow) and POSTs only the resulting token or PaymentIntent/PaymentMethod ID to the first-party endpoint. - Treat any third-party `<script src="...">` loaded on a payment-collection page without an `integrity` attribute (Subresource Integrity, e.g. a `sha256-` hash) as a finding — cite PCI-DSS v4 requirement 6.4.3 (script inventory, script authorization, and integrity control) as the standard-based grounding. Do not accept "we trust this vendor" as a substitute for a visible integrity attribute or equivalent cryptographic verification. - Stripe's hosted fields (`CardNumberElement`, `CardExpiryElement`, `CardCVCElement`, `PaymentElement`) render inside sandboxed iframes controlled by Stripe, not merchant code — the browser's same-origin policy and the iframe sandbox boundary are what keep merchant JavaScript from ever reading raw PAN/CVV values. A missing-iframe-isolation finding applies when card fields are collected via manual `<input>` elements with no sandboxed hosted-field equivalent in use. - When reviewing broader payment-page change-detection posture, note PCI-DSS v4 requirement 11.6.1 (automated change-detection/alerting for unauthorized script modification on payment pages) as a standard-based expectation — but do not fabricate a finding about a change-detection mechanism you cannot observe in the frontend code under review; note it as an open question instead. - Never execute, build, or run application code, and never send live requests, as part of this review; this is a static-review skill (Read/Grep/Glob only). - Load only the reference needed for the concern in scope. ## References Load these only when needed: - [Review workflow and findings contract](references/workflow-and-output.md) — use for the step-by-step review procedure, the defect decision tree, and the required output shape. - [Hosted fields and tokenization boundary](references/hosted-fields-and-tokenization.md) — load when reviewing card-entry form markup, `raw-pan-input`, `card-data-persistence`, or `self-posted-card-data` defect classes. - [Script integrity and SAQ-A scope reduction](references/script-integrity-and-scope.md) — load when reviewing third-party script tags for Subresource Integrity, or assessing whether a page's use of hosted fields plausibly supports SAQ-A scope-reduction claims. ## Response minimum Return, at minimum: - the payment-collection page(s), card-entry form markup, script tags, and/or client-side persistence/network calls in scope, - ranked findings with file:line evidence, defect category (`raw-pan-input`, `card-data-persistence`, `self-posted-card-data`, `unsigned-third-party-script`, or `missing-iframe-isolation`), the concrete data-flow trace (the raw `<input>`/`.value` read, the `localStorage`/analytics call, the POST body construction, or the missing `integrity` attribute), and a fix sketch matching Stripe's documented idiom (`CardNumberElement`, `PaymentElement`, `stripe.createToken`, or an `integrity`/SRI attribute), - for every finding involving PAN/CVV/cardholder data exposure, an explicit statement of whether the value stays inside a Stripe-controlled iframe (safe) or is reachable by merchant JavaScript (unsafe) — never approve on the assumption isolation exists without tracing it, - evidence level per finding (`repo evidence`, `documentation-based`, `standard-based`, or `inference`), with structural risk findings explicitly labeled as structural risk, not as confirmed-exploited or confirmed non-compliant, - verdict (approve / approve-with-notes / block), - open questions or scope the review could not cover — explicitly restate that this is **not a full PCI-DSS audit**: server-side cardholder-data-environment segmentation, key management, network controls, and non-script requirement families are out of scope and require a dedicated compliance/infrastructure review.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.