stripe-hardening-kit
Two small, real tools for anyone shipping a Stripe integration:
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.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,.mapby 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:
- 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).
- Calls
stripe.webhooks.constructEvent(rawBody, signature, webhookSecret). - Returns HTTP 400 immediately if verification throws — never processes an unverified event, no matter how well-formed it looks.
- Only after verification succeeds, handles
checkout.session.completedas the example event (extend theswitchfor 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.completedas 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)
| File | Size | Uploaded | |
|---|---|---|---|
| stripe_hardening_kit-1.0.0.tar.gz | 15.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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