Skip to main content

stripe-hardening-kit

Two small, real tools for anyone shipping a Stripe integration:

  1. scan_gate.py — a pre-deploy gate that scans your built/deployed site or app for banned/deprecated payment-integration patterns and blocks the deploy (non-zero exit code) if it finds one.
  2. webhook_reference.js — a minimal, correct, fail-closed Stripe webhook handler: verifies the signature before touching the payload, and rejects (HTTP 400) anything that doesn't verify.

Both come from the same real incident.

Why this exists

A project migrates its checkout from one payment provider to another — deprecated, banned by the new provider's terms, or just replaced by something better. The migration itself goes fine. Weeks later, a stray link or embed snippet referencing the old provider survives somewhere in the codebase — a copy-pasted template, an old component nobody deleted, a cached build artifact — and it gets redeployed. Checkout silently breaks: the button points at a dead or wrong destination, and because nothing throws an error and the page "looks fine," nobody notices until real revenue has already been lost.

The fix that actually works is the same one you'd use for any other class of regression: turn it into a gate that runs on every deploy, not a thing a human has to remember to check. scan_gate.py is that gate, generalized so you can point it at your own banned integration(s) — not just the one shipped as the default example.

The webhook handler addresses a related but separate risk: a Stripe webhook URL is public. Without signature verification, anyone who finds it can POST a forged event and trigger fulfillment (deliver a product, grant access, mark an order paid) for a payment that never happened. webhook_reference.js is a clean reference for doing verification correctly and rejecting everything else.

Part 1 — the deploy gate

Install

pip install -e .
# or, without installing:
python -m stripe_hardening_kit.scan_gate <target-dir>

This registers a console script:

stripe-hardening-scan <target-dir>

How it works

  • Scans every file under <target-dir> whose extension is in the "servable" set (default: .html, .htm, .js, .mjs, .cjs — override with --ext).
  • Skips dependency/VCS/cache directories by default (node_modules, .git, __pycache__, .venv, .cache, .next, .turbo, .vercel, etc. — extend with --skip-dir).
  • Skips files whose name contains a skipped substring (.bak, .log, .map by default — extend with --skip-substr).
  • Checks every line against a list of banned regex patterns.
  • Prints every match (file, line number, matched line, which pattern) and exits 1 if anything matched, 0 if clean.

Configuring your own banned patterns

The pattern list is a JSON file — nothing is hardcoded into the scanner. Copy the example and edit it:

cp banned_patterns.example.json banned_patterns.json
{
  "patterns": [
    { "name": "gumroad", "regex": "gumroad\\.com|gumroad_overlay|data-gumroad-product",
      "description": "Deprecated checkout provider — replaced by Stripe." },
    { "name": "my-old-processor", "regex": "checkout\\.old-provider\\.example" }
  ]
}

Then run:

stripe-hardening-scan ./dist --config banned_patterns.json

If you don't pass --config, the scanner uses a built-in default: Gumroad's known checkout-link and embed patterns. That's included as a real, concrete, working example — not a suggestion that Gumroad is the only thing worth banning. Add whatever your own project needs to keep out: an old payment processor, a deprecated analytics snippet, an internal/staging URL that should never reach production, anything else you've decided must never leak into a shipped file again.

Wiring it into a deploy pipeline

Run it as a required step before deploy, and fail the build on a non-zero exit code — the same way you'd wire up a test suite or a linter:

stripe-hardening-scan ./dist --config banned_patterns.json || exit 1
# only reached if the scan passed:
vercel deploy --prod

Or as a CI step (GitHub Actions example):

- name: Block banned payment integrations
  run: |
    pip install -e .
    stripe-hardening-scan ./dist --config banned_patterns.json

Running the tests

pip install -e ".[dev]"
pytest

The test suite creates real temporary files on disk (clean files, files with banned patterns, files inside skipped directories, custom pattern configs) and asserts the scanner passes/fails correctly in each case — see tests/test_scan_gate.py.

Part 2 — the webhook reference

webhook-example/webhook_reference.js is a minimal Stripe webhook handler that:

  1. Reads the raw request body (required — signature verification is computed over the exact bytes Stripe sent; a body already parsed by a framework's JSON middleware will never verify).
  2. Calls stripe.webhooks.constructEvent(rawBody, signature, webhookSecret).
  3. Returns HTTP 400 immediately if verification throws — never processes an unverified event, no matter how well-formed it looks.
  4. Only after verification succeeds, handles checkout.session.completed as the example event (extend the switch for whatever events you subscribe to).

Setup

cd webhook-example
npm install
cp .env.example .env.local   # fill in real values, never commit this file

Required environment variables (see .env.example):

  • STRIPE_SECRET_KEY — your Stripe API secret key.
  • STRIPE_WEBHOOK_SECRET — the signing secret for your specific webhook endpoint (Stripe Dashboard → Developers → Webhooks → your endpoint → "Signing secret"). Without it, the handler fails closed and rejects every request — by design, not a bug.

Wiring it up

Vercel / Next.js (pages API route) — the file is already written for this target. Drop it in as pages/api/stripe-webhook.js (or app/api/stripe-webhook/route.js adapted to the App Router's request/response shape) — the exported config.api.bodyParser = false disables Next's default body parsing so the raw bytes reach the handler untouched.

Express — mount the route with the raw body parser, not express.json():

const webhookHandler = require('./webhook_reference.js');

app.post(
  '/stripe-webhook',
  express.raw({ type: 'application/json' }), // req.body is now a raw Buffer
  (req, res) => webhookHandler(req, res)
);

(Adjust readRawBody() in webhook_reference.js to read from req.body directly instead of streaming req.on('data', ...), since Express's raw body-parser already buffers it for you.)

Register the endpoint in the Stripe Dashboard (or via the Stripe CLI for local testing: stripe listen --forward-to localhost:3000/stripe-webhook) and subscribe it to at least checkout.session.completed.

What's real here vs. generalized

  • scan_gate.py's scanning logic (served-extension filtering, skip-dirs, skip-substrings, line-by-line regex matching, exit-code semantics) is generalized from a gate that runs on every deploy of a real, live Stripe-only site — the pattern list is now a config file instead of one hardcoded provider name, so it's reusable for any banned integration, not just the one shipped as the default example.
  • webhook_reference.js's signature-verification pattern (raw body handling, constructEvent, fail-closed on verification failure, checkout.session.completed as the example event) mirrors a real production webhook handler's verification path, stripped of business-specific fulfillment logic (product catalogs, email delivery, per-customer routing) so it stands alone as a clean reference.

License

MIT — see LICENSE.

Metadata

Release files for stripe-hardening-kit 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for stripe-hardening-kit 1.0.0
File Size Uploaded
stripe_hardening_kit-1.0.0.tar.gz 15.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for stripe-hardening-kit 1.0.0
File Interpreter ABI Platform
stripe_hardening_kit-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 27.6 kB

Release files / stripe_hardening_kit-1.0.0.tar.gz

Download URL stripe_hardening_kit-1.0.0.tar.gz
Size 15.7 kB
Tags Source
SHA-256 checksum
How to use checksums
7e3ba3c0c8c37a45c6c1c60498fb7436eeffb2ba019efcfec3b5a557fab6e9d5
BLAKE2b-256 checksum
How to use checksums
a5442642a831e3afd1a3e941efee6dffac679e7b28d89de327191732ca9fa282
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release files / stripe_hardening_kit-1.0.0-py3-none-any.whl

Download URL stripe_hardening_kit-1.0.0-py3-none-any.whl
Size 11.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fdc01320c0befe9b4a18475bc356f438a4c365841e99d44fe545a16519aba91c
BLAKE2b-256 checksum
How to use checksums
8b7c1ac356c18ba4df1b92f03b5dc280d34c7f8e7d92d7951364c6278230fd39
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page