3.7 KiB
3.7 KiB
2026-07-17 — Paynow inbound webhook signature verification + .env.example
Branch: fix/paynow-verify-and-env-example
Worktree: .worktrees/fix-paynow-verify-and-env-example
Why
The deployment-readiness audit listed Paynow inbound webhook signature
verification as a P0 security gap. The webhook handler in
server/src/controllers/payments.controller.js trusted the request body
unconditionally, allowing anyone who knew a payment reference to flip a
pending payment to completed (or worse, mark a fee paid).
A second gap was the absence of .env.example in the repo, forcing new
contributors to grep process.env.* to discover the runtime contract.
Changes
server/src/controllers/payments.controller.js
- Added
verifyPaynowHash(payload)— symmetric with the existing outboundgenerateHash. Sorts non-empty fields, concatenateskey+value, appends the integration key, SHA-1 hex digest. Compares withcrypto.timingSafeEqualafter a length check (the latter throws on mismatched lengths). - New
PAYNOW_SKIP_VERIFYenv flag, defaultfalse. Whentrue(dev / tests only), the verifier is bypassed. Bypass is impossible whenPAYNOW_INTEGRATION_KEYis unset — that path still succeeds, which is the existing test-mode behaviour. - Webhook handler now rejects with
401and a structured log line before any DB read/write when the signature fails. The error message intentionally does NOT leak the expected hash. - The integration-key check fires only when an integration key is configured on the server — preserves current test-mode behaviour for greenfield installs.
.env.example (new, repo root)
Documents every env var consumed by server/src/** and the VITE_* mirror
needed for client build-time inlining. Sections:
- Node / runtime (
NODE_ENV,PORT,DB_PATH) - Auth (
JWT_SECRET,ALLOWED_ORIGINS,ALLOW_CLIENT_OFFLINE_MINT) - Paynow (
PAYNOW_INTEGRATION_ID,PAYNOW_INTEGRATION_KEY,PAYNOW_RETURN_URL,PAYNOW_BLOCKING_URL,PAYNOW_SKIP_VERIFY) - Supabase (
SUPABASE_URL,SUPABASE_KEY,VITE_SUPABASE_URL,VITE_SUPABASE_ANON_KEY) - SMTP (
SMTP_HOST,SMTP_PORT,SMTP_SECURE,SMTP_USER,SMTP_PASS,SMTP_FROM) - Sync tuning (
SYNC_INTERVAL)
client/e2e/paynow.spec.ts (new)
Four Playwright specs covering the new behaviour:
- Webhook with no
hashfield → 401. - Webhook with a 40-char wrong hash → 401 (exercises
timingSafeEqual). - Webhook with a correctly signed payload → 200 or 404 (both prove the verifier accepted the signature).
- Skip-when-unconfigured guard: tests skip cleanly when
PAYWOW_TEST_INTEGRATION_KEYis not set on the server (e.g. local dev without Paynow credentials).
Verification
- The file is small and surgical. The verifier mirrors the existing outbound hash so any future schema change there must be mirrored here in lockstep.
- The bypass env flag is named clearly and its effect is gated by both an explicit boolean and the integration-key check.
Risk
- Paynow's actual inbound callback field list may differ slightly from the
outbound schema this code generates. If Paynow adds or omits a field in
the callback, the verifier's sort-and-concat will diverge from Paynow's
signature and every real webhook will fail with 401. Mitigation: log the
hash_mismatchreason with the IP and an opaque reference-free payload digest so we can spot it on first real payment; the merchant can then publish the documented field list and we tweak the verifier. - Existing local-only tests (none currently drive the webhook) are
unaffected because
PAYNOW_INTEGRATION_KEYis unset in test setups and the verifier is therefore bypassed.