Payments
Payment Security for Websites: What You Control and What the Processor Handles
Payment security is mostly about not becoming a card-data warehouse. Processors exist so your application can take money without storing primary account numbers (PAN). Your remaining job is to protect accounts, sessions, webhooks, and the pages around checkout so an attacker cannot mark orders paid, steal logins, or inject a fake total.
This is not a PCI-DSS manual. If you never touch raw card data, your PCI scope is smaller, but it is not zero. You still run a website that can be phished, scraped, or used as a malware host. Treat the store like a financial application even when Stripe hosts the card fields.
Do not store raw PAN — including in the places people forget
Full card numbers, CVV, and track data must not be written to your database, log files, error trackers, chat transcripts, or email. Developers often leak PAN through:
- Request logs that dump POST bodies.
- Exception reporters that include form input.
- Support tools that paste a “card the customer read over the phone.”
- Staging databases copied from production.
Use processor-hosted Checkout, a Payment Element, or another tokenization UI so the browser talks to the processor for card data. Your server should see payment method IDs, customer IDs, and amounts you calculated yourself. If a vendor asks you to email a card number to “complete setup,” that is a reason to stop, not a workaround.
If you ever take cards by phone, use a processor virtual terminal. Do not type PAN into your admin UI.
HTTPS everywhere that leads to money
TLS belongs on the whole site, not only /checkout. Mixed content, an HTTP marketing page that posts to HTTPS, or a stale certificate all train users to ignore warnings. Redirect HTTP to HTTPS, enable HSTS when you are sure you will stay on TLS, and keep certificates auto-renewed.
Cookies for sessions must be Secure and HttpOnly, with SameSite set deliberately. Checkout that relies on a session cookie you can steal over coffee-shop Wi-Fi is not a payment integration problem; it is a basic web problem. Broader practices sit in secure web development practices.
Webhooks: verify signatures, then be idempotent
Processors tell you about payments with HTTP callbacks. Anyone who can guess your URL can POST a JSON body that says the order was paid unless you verify the signature (Stripe’s signing secret, similar HMAC schemes elsewhere).
Rules that prevent expensive mistakes:
- Verify the signature against the raw request body before you parse business logic.
- Reject old timestamps if the processor includes them, to limit replay.
- Store processed event IDs so retries do not double-fulfill.
- Do not fulfill from a browser redirect alone.
- Keep webhook secrets out of the repository; rotate if they leak.
Your webhook endpoint should not be a debug dump. Returning stack traces or echoing the payload to a public log is how you publish customer emails and payment IDs.
Amounts, SKUs, and the client are not trusted
Price lives on the server. The browser may display a total; it must not be allowed to set the charge. Bind the PaymentIntent or Checkout Session to line items you loaded from your catalog. If you support coupons, validate them server-side against expiry and usage limits.
Admin tools that can issue refunds or change prices need authentication commensurate with money: unique logins, 2FA for staff who can refund, and an audit log. A shared “admin / admin” account on a store is a payment incident waiting for a former contractor.
Accounts, password reset, and support
Stolen customer accounts are a payment problem: saved payment methods, address books, and reorder buttons. Use hashed passwords, rate-limit login and reset, and treat support impersonation carefully. A phone caller who knows an order number is not automatically the cardholder.
Email is a weak reset channel if the inbox is already compromised. It is still what most stores use. At least expire reset tokens, and notify the user when a new device logs in if you can do that without locking out legitimate travel.
Infrastructure around checkout
- Keep PHP and dependencies updated; payment libraries are not exempt.
- Use parameterized queries so an order-id parameter cannot dump the customer table.
- Restrict admin panels by network or SSO if the team is small enough to make that practical.
- Back up the database; test restores. Ransomware on an unpaid-order table is still ransomware.
- Content Security Policy can reduce card-skimming scripts injected via a compromised plugin. It is not a substitute for patching.
Third-party scripts on the checkout page expand the number of people who can run JavaScript next to the pay button. Load only what you need. A heatmap tool that watches the payment page is a business decision with a security cost.
Fraud is not the same as PCI, but customers feel both
Processors offer radar-style rules, 3-D Secure, and velocity checks. Turn on 3-D Secure where your market expects it rather than fighting every challenge. Watch dispute rates; they affect whether you keep the account. Velocity limits on coupon abuse and new-account checkout can be implemented in your app even when the processor handles the card risk.
Do not block entire countries as a substitute for understanding where your real customers are. Do log enough to investigate a dispute: IP, device-ish signals you are allowed to keep, and the shipping address you actually used.
A short control list before you take live payments
- No PAN in database, logs, or tickets.
- TLS site-wide; secure session cookies.
- Webhook signatures verified; fulfillment idempotent.
- Prices and SKUs enforced server-side.
- Staff refund access limited and logged.
- Dependencies and plugins patched on a calendar, not a feeling.
If you want these controls built into a PHP store or a hybrid checkout, Scriplit can do that as part of payment gateway integration. Security work is cheaper before the first live card than after a leaked log file. Pair it with ordinary site hygiene so checkout is not the only hardened URL on an otherwise neglected host.