fix(rate-limit): rate-limit /internal/webhooks/replay and fix pre-existing keyGen bug - #150
Open
Whiznificent wants to merge 2 commits into
Open
Conversation
…sting keyGen bug Fixes Vero-protocol#130 Problem - POST /internal/webhooks/replay had JWT verification but no rate limit, so a compromised/leaked internal token could replay-flood the queue path unbounded. Changes - index.js: place ingestRateLimiter before verifyJwtBearer on /internal/webhooks/replay so the limiter throttles even before JWT verification runs. - src/middleware/rateLimit.js: fix clientIp() to call ipKeyGenerator(req.ip) (string) rather than ipKeyGenerator(req) (request object). The previous version returned { ip: "..." } - an object - which silently broke hit-tracking: express-rate-limit's default Map-backed store keys on object references, so every request was its own bucket and the rate limit never engaged. Verifying IPv6 subnet-keying still works as documented by the upstream helper. - test/rateLimit-internal-replay.test.js: new integration test that pins RATE_LIMIT_AUTH_MAX=2 before any require, drives createApp with mocked fetchRawEvent/enqueueEventJob, sends three authenticated requests from the same X-Forwarded-For IP, asserts first two are 202 and third is 429 with code RATE_LIMIT_EXCEEDED. Restores env vars in after() so sibling tests are unaffected. Side effects - The pre-existing failing test "ingestRateLimiter respects RATE_LIMIT_PUBLIC_MAX env when reloaded (can trigger 429)" now passes too, because the underlying keyGenerator bug is fixed. Verification - node --test test/rateLimit-internal-replay.test.js -> 1 pass - node --test test/rateLimit.test.js -> 17 pass - node --test test/rateLimit.test.js test/rateLimit-internal-replay.test.js test/webhook.test.js -> 22 pass, 1 unrelated failure (github-webhook enqueue test was already failing on this branch due to missing REDIS_HOST infrastructure; unrelated)
N-thnI
approved these changes
Aug 29, 2026
| app.post('/internal/webhooks/replay', verifyJwtBearer, async (req, res) => { | ||
| // Internal replay endpoint — rate-limited before JWT verification so a | ||
| // compromised/leaked internal token cannot replay-flood the queue path. | ||
| app.post('/internal/webhooks/replay', ingestRateLimiter, verifyJwtBearer, async (req, res) => { |
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.
closes #130
Summary
ingestRateLimiterto the/internal/webhooks/replaymiddleware chain so a compromised or leaked internal JWT cannot replay-flood the queue.src/middleware/rateLimit.jsclientIp(req)that was silently disabling every rate limiter in the codebase (it was passing the request object toipKeyGenerator, which caused the helper to return an object literal — and the default store keys off object references, so every request landed in its own bucket and hits never accumulated).Problem
POST /internal/webhooks/replayrequires a valid JWT (verifyJwtBearer) but unlike/github-webhookhad no rate limiter in its middleware chain. A leaked or compromised internal token could replay-flood the queue/on-chain submission path with no throttle.Changes
New files
test/rateLimit-internal-replay.test.js— integration test that pinsRATE_LIMIT_AUTH_MAX=2before anyrequire, drivescreateAppwith mockedfetchRawEvent/enqueueEventJob, sends three authenticated supertest requests from the sameX-Forwarded-ForIP, and asserts the first two are202and the third is429withcode: "RATE_LIMIT_EXCEEDED". Restores env vars inafter()so sibling test files are unaffected.Modified files
index.js— appendingestRateLimiterbeforeverifyJwtBeareron the/internal/webhooks/replayroute so the limiter throttles even before JWT verification runs.src/middleware/rateLimit.js— fixclientIp(req)to callipKeyGenerator(req.ip)(the IP string), matching whatexpress-rate-limitv8+ expects. The previousipKeyGenerator(req)call returned an object like{ ip: "..." }, which silently broke Map-backed hit tracking and made every rate limit a no-op. IPv6-subnet keying still works as documented by the upstream helper.Verification
As a side effect, the previously-failing test
ingestRateLimiter respects RATE_LIMIT_PUBLIC_MAX env when reloaded (can trigger 429)now passes because the underlyingclientIpbug is fixed.Security notes
RATE_LIMIT_AUTH_MAX) applies because every request to this endpoint carries anAuthorization: Bearer …header.Notes
src/middleware/rateLimit.jsAPI surface, configuration, or exports — purely a correctness fix toclientIp.RATE_LIMIT_AUTH_MAXindependently if desired.