@boring-stack-pkg/eslint-plugin-stripe-webhooks
v0.1.2
Published
ESLint plugin enforcing security and correctness rules for Stripe webhooks.
Maintainers
Readme
eslint-plugin-stripe-webhooks
ESLint plugin enforcing security and correctness rules for Stripe webhook handlers.
Why
Stripe webhooks have two infamous failure modes:
- Unverified payloads. Reading or parsing the request body before
*.constructEvent(...)succeeds means an attacker can deliver a webhook-shaped payload to your endpoint and your handler will fire database writes / queue jobs / emails on it. The signature check never gets a chance to reject it. - Non-idempotent and type-blind handling. Stripe redelivers events on transient failures — a handler without a dedupe check on
event.idwill double-charge, double-email, double-publish. A handler that doesn't branch onevent.typewill run identical logic for every event kind.
These six rules pin down the patterns that prevent both.
Install
pnpm add -D @boring-stack-pkg/eslint-plugin-stripe-webhooks @typescript-eslint/parserUsage (flat config)
// eslint.config.mjs
import tsParser from "@typescript-eslint/parser";
import stripeWebhooks from "@boring-stack-pkg/eslint-plugin-stripe-webhooks";
export default [
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
},
plugins: { "stripe-webhooks": stripeWebhooks },
rules: stripeWebhooks.configs.recommended.rules,
},
];The recommended preset enables all six rules at "error". Override per-rule via the standard ESLint mechanism.
Rules
| Rule | Tier | Description |
| ---------------------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| handler-must-verify-signature | TIER 1 SECURITY | Disallow reading or forwarding the webhook payload before *.constructEvent(...) succeeds. |
| no-parsed-body-before-verification | Security | Disallow parsed-body APIs (request.json(), JSON.parse(body), req.body, express.json()) before verification. |
| require-stripe-signature-header | Security | Require the signature passed into constructEvent(...) to come from the Stripe-signature header; forbid hard-coded whsec_* secrets. |
| handler-must-handle-event-type | Correctness | Stripe event handlers must branch on event.type. |
| handler-must-be-idempotent | Correctness | Webhook handlers performing side effects must consult event.id for dedupe. |
| service-must-construct-event | Convention | Stripe-aware classes with a webhook-named method must also have a verifier method calling constructEvent. |
Examples
handler-must-verify-signature
// ❌
export async function POST(request: Request) {
const body = await request.json();
const event = stripe.webhooks.constructEvent(body, sig, k);
}
// ✅
export async function POST(request: Request) {
const body = await request.text();
const sig = request.headers.get("stripe-signature");
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.WEBHOOK_SECRET!,
);
}handler-must-be-idempotent
// ❌
export async function handle(event: Stripe.Event) {
if (event.type === "payment_intent.succeeded") {
await db.payments.insert({
/* ... */
});
}
}
// ✅
export async function handle(event: Stripe.Event) {
if (await alreadyProcessed(event.id)) return;
if (event.type === "payment_intent.succeeded") {
await db.payments.insert({
/* ... */
});
}
}For complete per-rule docs and ❌/✅ snippets, see docs/rules/ and the runnable examples/ (Next.js App Router, Express, Elysia/Hono, class-based services).
Security philosophy
These rules are best-effort static analysis. They catch the common mistakes — reading req.body before constructEvent, hard-coded whsec_* secrets, missing event.type branching — but they cannot prove a webhook handler is correct. Specifically, no rule here is sufficient on its own. Pair them with:
- End-to-end tests against the Stripe CLI webhook simulator.
- A TIER 1 security review of every webhook entry point at PR time.
- Centralized verification (see
service-must-construct-event) so the verification step lives in one place.
Limitations of static analysis
- No cross-function dataflow. A body parameter passed into a helper in another file isn't tracked.
- No type-aware inference.
Stripe.Eventparameter detection relies on the literal type name + a Stripe import being present in the same file. Generic wrapper types (MyEvent<Stripe.Event>) aren't recognized. - Idempotency check detection is heuristic. Helper functions abstracting the dedupe logic must be named per
allowedCheckFunctionPatterns. DB unique-constraint-based dedupe isn't visible. - Verification by middleware that this rule doesn't recognize will look like "no verification" to the rule.
When in doubt, prefer the explicit, in-handler constructEvent call over a clever abstraction — it's the pattern the rules optimize for.
Development
pnpm install
pnpm test
pnpm typecheck
pnpm buildRelease
Tag a v* version locally and push the tag. .github/workflows/release.yml runs pnpm publish --access public --no-git-checks with NPM_TOKEN.
License
MIT.
