
Local static security scanner for Next.js, Supabase and Stripe apps: 24 AST-based checks for leaked keys, missing RLS and grants, unsafe webhooks. CLI and GitHub Action.
Free, local security scan for Next.js + Supabase + Stripe apps: leaked keys, missing RLS and grants, unsafe Stripe webhooks.
Your AI agent shipped the app. Did it also ship your Stripe secret key, a table without RLS, or a webhook that anyone can call? One command finds out, locally, in under a second.
npx github:edtrackai/nextjs-supabase-stripe-security-checker /path/to/your/app
It isn't on npm yet, so npx runs it straight from this GitHub repo (needs Node 18+ and git).
- uses: edtrackai/nextjs-supabase-stripe-security-checker@v0
with:
fail-on: high
Full workflow and permissions are below and in docs/github-action.md.
Supabase RLS checker · Stripe webhook security · Next.js security scan · Supabase Data API grants (42501)

git clone https://github.com/edtrackai/nextjs-supabase-stripe-security-checker.git
cd nextjs-supabase-stripe-security-checker && npm install
node bin/nss-security-check.js /path/to/your/app
Node 18 or later. There's one dependency (@babel/parser). It's free and MIT-licensed, needs no account, makes no network calls and never uploads your code.
--fail-on=high exits 1, and --json gives machine-readable output.Ad (paid, same author): Want the fixes? Launch Audit ($12 plus VAT where applicable) adds 28 checks, an AI auditor agent with no Edit or Write tools, and a file:line fix plan.
| Flag | What it does |
|---|---|
--json | Machine-readable output: {tool, version, root, filesScanned, counts, findings[]} |
--fail-on=<sev> | Exit 1 if any finding is at or above critical / high / medium / low (none never fails). Exit 2 on bad arguments. |
--list | Print the check catalogue |
--quiet | Omit the one-line footer about the paid tools (printed to stderr on a terminal only; also off with NO_PROMO=1, CI or GITHUB_ACTIONS) |
--help | Usage |
In CI: node path/to/bin/nss-security-check.js . --fail-on=high, or use the GitHub Action (job summary, annotations, one sticky PR comment, only GITHUB_TOKEN). See the example workflow and permissions in docs/github-action.md.
# .github/workflows/security-check.yml
name: Security check
on: [pull_request]
permissions:
contents: read
pull-requests: write # only for the PR comment; drop it with comment: false
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: edtrackai/nextjs-supabase-stripe-security-checker@v0 # or pin @v0.5.0 / a commit SHA
with:
path: .
fail-on: high
Changes are listed in CHANGELOG.md (0.5.0 adds the Action).
| ID | Severity | Area | Check |
|---|---|---|---|
| K1 | critical | Keys | Live Stripe secret/restricted key (sk_live_/rk_live_) in source, including keys split with + (evidence is redacted) |
| K2 | critical | Keys | Secret-looking NEXT_PUBLIC_ variable (service role, secret key, webhook secret…) |
| K3 | critical | Keys | .env file with values tracked by git |
| K4 | critical | Keys | Server secret or Supabase admin client used in a "use client" component |
| A1 | high | Auth | auth.getSession() trusted on the server instead of getUser()/getClaims() |
| A2 | high | Auth | Server Action writes data with no auth check |
| A3 | high | Auth | API route (POST/PUT/PATCH/DELETE) writes data with no auth check (webhook, cron and auth routes excluded) |
| A4 | high | Auth | Admin client queried by a request-supplied id with no owner filter (IDOR) |
| A5 | medium | Auth | Open redirect from a query parameter |
| R1 | critical | RLS | Table created without row level security |
| R2 | critical | RLS | Write policy with using (true) / with check (true) |
| R3 | high | RLS | Policy that lets the anon role write |
| R4 | high | RLS | security definer function without a fixed search_path |
| R5 | medium | RLS | Table in public with no explicit GRANT. Supabase announced it would stop default grants on new public tables from 30 Oct 2026, but in late August a Supabase CLI maintainer wrote (supabase/cli PR #6337) that the earlier default change "never landed" and hosted projects were still auto-granting, so the date may move: check what your own project does. Without a grant, the Data API returns 42501 |
| R6 | high | RLS | Blanket grant ... on all tables in schema public / alter default privileges ... grant to anon/authenticated (the quick 42501 fix that also exposes tables without RLS) |
| S1 | critical | Stripe | Webhook without signature verification (constructEvent) |
| S2 | critical | Stripe | Webhook parses request.json() instead of the raw body |
| S3 | medium | Stripe | Webhook has no idempotency on event.id |
| S4 | high | Stripe | Checkout price or amount taken from the request |
| S5 | high | Stripe | Access granted on the checkout success page instead of in the webhook |
| S6 | medium | Stripe | Webhook dedupe that can swallow a failed event: duplicates skipped with no pending/completed status |
| S7 | low | Stripe | Billing period read from items.data[0] (wrong for multi-item subscriptions since API 2025-03-31.basil) |
| H1 | medium | Hardening | No security headers in next.config |
| H2 | medium | Hardening | dangerouslySetInnerHTML with a non-literal value |
fixtures/vulnerable-app triggers all 23 file-based checks, and the tests assert each one at the expected file. K3 is tested on a temporary git repo.fixtures/clean-app (a hardened reference app) and fixtures/near-miss (safe code that looks like each mistake: raw-body webhooks with pending/completed idempotency, multi-item-safe period lookup, guarded redirects, owner-filtered admin queries, select using (true), JSON-LD) must produce 0 findings.npm test runs the node:test suite, including the CLI flags and exit codes. npm run smoke gives a one-line summary.