Repository navigation
fix(security): resolve PR #1703 review findings (CSRF FPs, secret fixture) - #1711
Merged
github-actions[bot] merged 1 commit intoOct 16, 2026
Merged
Conversation
… secret fixture) Address the two unresolved review threads left on PR Smartdevs17#1703. ## Semgrep: state-changing-route-without-csrf (x2) `subtrackr.csrf.state-changing-route-without-csrf` matched app.post('/api/v1/stripe/billing/webhook', express.raw({ ... })) because the rule only recognises a middleware literally named `csrfProtection`, `requireAuth` or `verifyWebhookSignature`, and it cannot follow the call into `createStripeBillingRouter` / `createPaddleRouter`. Both are false positives. Those two lines install a *body parser only*: they declare no handler and change no state. The routes are mounted further down, and both fail closed: - 503 when the signing secret is unconfigured; - 401 when the HMAC over the raw bytes does not match (`timingSafeEqual` plus a 300s replay window). The obvious "fix" of re-running verification at the app level would be a regression. `StripeWebhookVerifier.verify()` calls `remember(eventId)` on success, so a second call in the same request returns `fresh: false`, and the router would answer `200 { duplicate: true }` without ever dispatching the event, silently dropping every subscription lifecycle update. The suppression records that constraint so nobody repeats it. Registered in `.semgrep/semgrep-suppressions.yml` with a 6-month expiry so the exception stays auditable. ## GitGuardian: hardcoded Zapier webhook URL `outboundUrlGuard.test.ts` asserted on `https://hooks.zapier.com/hooks/catch/1/2/`. It is a fixture, not a secret, but the path shape is indistinguishable from a live hook URL and created a false incident. Swapped for a non-`/hooks/catch/` path on the same host: the assertion only needs "public host over https", and the realistic Zapier hostname coverage is kept. Verification: `node --experimental-strip-types --check` on both touched TS files. Lint, typecheck and tests not run, `node_modules` is not installed here.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1703, which is already merged. This clears the two review
threads that were left unresolved there.
Semgrep:
subtrackr.csrf.state-changing-route-without-csrf(x2)The rule fired on these two lines in
backend/server/createApiServer.ts:These are false positives. Both lines install a body parser only — they
declare no handler and change no state. The rule matches
$ROUTER.post($PATH, $HANDLER)and excepts only a middleware literally namedcsrfProtection,requireAuthorverifyWebhookSignature; it cannot follow the call intocreateStripeBillingRouter/createPaddleRouterin other files.The routes are genuinely protected, and they fail closed:
verifier === null)503401200 { duplicate: true }, never dispatchedThe digest is compared with
crypto.timingSafeEqualinside a 300 s replaywindow (
StripeWebhookVerifier.verify/PaddleWebhookVerifier.verify).Why this is not fixed by adding middleware to the route
The tempting fix — running
verify()again at the app level — is a silentproduction regression.
verify()callsremember(eventId)on success, so asecond call within the same request returns
fresh: false, and the routerthen answers
200 { duplicate: true }without ever dispatching the event.Every Stripe/Paddle subscription lifecycle update would be dropped.
So this lands as a documented suppression (inline
nosemgrepplus an entry in.semgrep/semgrep-suppressions.ymlwith a 6-month expiry), with thedouble-verification trap written into the code comment so the next reviewer
does not "fix" it the obvious wrong way.
GitGuardian: hardcoded Zapier webhook URL
outboundUrlGuard.test.tsasserted on:A fixture, not a secret — but the
/hooks/catch/<n>/<n>/path shape isindistinguishable from a live hook URL, so it raised a false incident. Replaced
with a non-
/hooks/catch/path on the same host. The assertion only needs"public host over https", and realistic Zapier-hostname coverage is kept.
Verification
node --experimental-strip-types --checkpasses on both touched TS files.nosemgrepjustification gate fromsecurity-scan.ymlwas reproducedlocally and passes (every suppression line carries a
reason:).npm run lint,npm run typecheckand the Jest suites were not run —node_modulesis not installed in this environment. CI will cover those.