fix(security): close the shot-render rate-limit bypass and gate it with a signed token - #9210
Merged
Merged
Conversation
…th a signed token (#9044) /loopover/shot is the only internet-facing route on the self-host topology, and its per-identity rate limiter keyed exclusively on cf-connecting-ip -- correct on Cloudflare Workers, but blind behind a Cloudflare Tunnel to a plain Node process, where a caller-supplied header could be rotated to mint a fresh bucket every request (or share one "unknown-ip" bucket with everyone, if no header at all reached the origin). - Thread the genuine Node-runtime TCP peer address into the request env (server.ts, via @hono/node-server's own second FetchCallback argument, previously ignored) and make clientIp() trusted-proxy-aware: only honor cf-connecting-ip/X-Forwarded-For when the immediate peer is loopback (the only thing that can ever reach this deployment's tunnel-fronted port); otherwise key on the real peer address directly, never a shared "unknown-ip" bucket. Cloudflare Workers' existing behavior (no peer info) is unchanged. - Add a fixed-key, identity-independent global ceiling on the on-demand render mode specifically, so rotating the header can never manufacture more than one shared bucket regardless of how well IP attribution holds up in a given topology. - Gate the render (?url=) mode behind a signed, expiring token (shot-render-token.ts) minted when the review comment embeds the link, validated before the existing host-allowlist/SSRF checks (kept as defense-in-depth underneath, not replaced). - Add a best-effort DNS-resolution pin ahead of navigation, closing the common case of a public hostname whose A record resolves to a disallowed address; not a substitute for a real pinned connection, which Puppeteer/CDP interception has no API to force. - Fix a stale doc comment claiming this route is rate-limited as "normal" -- it is "expensive". Tests: rotating-identity bypass attempts still throttled by the global ceiling, trusted-proxy clientIp behavior (loopback vs non-loopback peer, header vs no-header), the signed token's mint/ verify contract (valid/tampered/expired/wrong-url), and the DNS-pin's block/fail-open paths.
Contributor
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Contributor
Logic backtestReplayed 0 historical case(s) for Backtest comparison:
|
❌ 4 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
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.
Summary
/loopover/shotis the only internet-facing route on the self-host topology, and its per-identity rate limiter keyed exclusively oncf-connecting-ip— correct on Cloudflare Workers, but blind behind a Cloudflare Tunnel to a plain Node process, where a caller-supplied header could be rotated to mint a fresh bucket every request (or share one "unknown-ip" bucket with everyone, if no header at all reached the origin).server.ts, via@hono/node-server's own second FetchCallback argument, previously ignored) and makeclientIp()trusted-proxy-aware: only honorcf-connecting-ip/X-Forwarded-Forwhen the immediate peer is loopback (the only thing that can ever reach this deployment's tunnel-fronted port); otherwise key on the real peer address directly, never a shared "unknown-ip" bucket. Cloudflare Workers' existing behavior (no peer info) is unchanged.?url=) mode behind a signed, expiring token (shot-render-token.ts) minted when the review comment embeds the link, validated before the existing host-allowlist/SSRF checks (kept as defense-in-depth underneath, not replaced).Test plan
npx tsc --noEmit -p tsconfig.json --incremental false— cleannpm run db:migrations:check/npm run db:schema-drift:check/npm run branding-drift:check— all oknpx vitest run test/integration/shot-route.test.ts test/unit/auth.test.ts test/unit/shot-render-token.test.ts test/unit/visual-capture.test.ts test/unit/visual-shot.test.ts test/unit/index.test.ts— 422 passedclientIpbehavior (loopback vs non-loopback peer, header vs no-header), the signed token's mint/verify contract (valid/tampered/expired/wrong-url), and the DNS-pin's block/fail-open pathsmainafter landing (picked up fix(orb): close-authority integrity across four review-engine gaps #9206's close-authority fix, fix(test,docs): resync installation-health fixtures and self-host docs with the #9169 required-events expansion #9203 — no conflicts, re-verified)Closes #9044