Skip to content

test(limiter): pin rate-limiter behavior — limits, 429, exclusions, identities, documented fail-open - #449

Closed
Alajmah wants to merge 1 commit into
masterfrom
security/limiter-behavior-regressions
Closed

Alajmah wants to merge 1 commit into
masterfrom
security/limiter-behavior-regressions

Conversation

@Alajmah

@Alajmah Alajmah commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Unit C of the authorized dismissal-gate sequence — test-only, no limiter changes. Seven regressions pin the exact semantics the 13 API-alert false-positive dismissals rest on:

  1. Applicable requests pass and carry X-RateLimit-* headers.
  2. The 121st request in the window → 429 with Retry-After: 60.
  3. Distinct Bearer identities get independent windows (key C1 saturated does not affect key C2).
  4. Requests without Bearer credentials fall back to IP identity (shared per-IP bucket) — implementing the matrix's corrected identity statement.
  5. /health excluded entirely — no Redis interaction across 130 requests.
  6. /webhooks* excluded.
  7. Documented fail-open: a Redis failure currently allows the request through (logged). Pinned deliberately — flipping to fail-closed is an availability trade-off reserved to a separate decision, and this test must change with that decision, never silently.

Holding at the merge gate — exact head 0f3c412d3df2417a46e5fd5b2e279250cf319126.

…dentities, documented fail-open

Unit C of the #425 dismissal-gate sequence (test-only; NO limiter
changes). Seven regressions pin the existing semantics that the
13 API-alert false-positive dismissals rest on:

- applicable requests pass and carry X-RateLimit headers
- the 121st request in the window → 429 + Retry-After 60
- distinct Bearer identities get independent windows
- requests without Bearer credentials fall back to IP identity
  (shared per-IP bucket) — correcting the matrix's identity statement
- /health excluded entirely (no Redis interaction, unlimited passage)
- /webhooks* excluded
- DOCUMENTED FAIL-OPEN: a Redis failure currently allows the request
  through (logged). Pinned deliberately — changing to fail-closed is
  an availability trade-off reserved to a separate decision, and this
  test must change WITH that decision, never silently.

Signed-off-by: Alajmah <55954113+Alajmah@users.noreply.github.com>
@Alajmah

Alajmah commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

/gitwire run review

@gitwire-hq

gitwire-hq Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

▶️ GitWire: Re-evaluation triggered for review. Results will appear shortly.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T06:13:17.607265Z 0f3c412 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f3c412d3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +102 to +105
it("requests WITHOUT Bearer credentials fall back to IP identity", async () => {
const a = await runMiddleware({ ip: "10.0.0.1" });
const b = await runMiddleware({ ip: "10.0.0.2" });
const a2 = await runMiddleware({ ip: "10.0.0.1" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cover non-Bearer Authorization headers in the identity test

This exercises only a missing Authorization header, so it passes even though rateLimiter currently applies slice(7) to every authorization scheme. Requests such as Authorization: Basic attacker-1 therefore use the sliced header as their identity instead of the IP, and a client can vary that value to evade the shared IP bucket while this purported regression remains green. Include a non-Bearer or malformed header in this case so the claimed fallback is actually verified.

AGENTS.md reference: AGENTS.md:L111-L114

Useful? React with 👍 / 👎.

redisState.counts.set(key, next);
return next;
}),
pexpire: jest.fn(async () => 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify that the rate-limit window actually expires

The Redis mock stores counts indefinitely and its pexpire implementation has no effect, so the suite would still pass if the production middleware stopped setting a TTL or used the wrong duration. In that regression, an identity that reaches 120 requests would remain blocked permanently rather than only for the documented minute. Model time/expiry or at least assert the pexpire call and duration, then verify admission after expiration.

Useful? React with 👍 / 👎.

Comment on lines +131 to +132
expect(passed).toBe(true);
expect(res.statusCode).toBe(200);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert the logging half of the fail-open contract

This case claims that a Redis failure is logged, but it checks only passage and status, so removing the logger.error call would leave the test green and silently violate the repository's error-logging requirement. Retain a reference to the mocked logger and assert the expected log invocation for ECONNREFUSED.

AGENTS.md reference: AGENTS.md:L103-L105

Useful? React with 👍 / 👎.

@gitwire-hq gitwire-hq Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Code Review — ✅ Approved

Confidence: high · Findings: 2
AI judgment: APPROVE · Evidence: COMPLETE · Authority: advisory
Evidence complete · 1/1 changed files accounted for

Other findings (2)

  • [medium] Fail-open test never asserts the logging behavior its title and the PR description pin
  • [low] Unused makeApp helper is dead code whose mount order would produce vacuous tests if ever used

🔮 Devil's Advocate (2 turns): 2 upheld


GitWire AI Review Gate (bundle-driven v2) · Structured schema · Scope-validated · Devil's Advocate · AI recommendation — maintainers and repository policy retain authority

expect(redisState.counts.size).toBe(0);
});

it("DOCUMENTED FAIL-OPEN: a Redis failure currently allows the request through and logs", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Fail-open test never asserts the logging behavior its title and the PR description pin

The test is titled 'DOCUMENTED FAIL-OPEN: a Redis failure currently allows the request through and logs', and the PR description states the fail-open is '(logged)'. However, the test never imports the mocked logger module and never asserts that logger.warn/error was called, so the 'logged' half of the pinned contract is unverified. A regression that removes the warning log (fail-open goes silent, losing all observability of limiter bypass) would keep this suite green. Additionally, expect(res.statusCode).toBe(200) is tautological: the mock res initializes statusCode to 200 and passed=true already proves next() was invoked. Suggested fix: after the middleware import, add const { logger } = await import("../../src/lib/logger.js"); and in this test assert e.g. expect(logger.warn).toHaveBeenCalled() (or whichever level the middleware uses), keeping the log assertion tied to the same 'must change with the fail-closed decision' comment.


const express = (await import("express")).default;

function makeApp() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW] Unused makeApp helper is dead code whose mount order would produce vacuous tests if ever used

makeApp() builds an Express app but mounts app.use(rateLimiter) AFTER the route handlers. Since the routes terminate the response, the limiter would never execute for any request through that app, so any future test written against makeApp would pass vacuously while asserting nothing about the limiter. The author clearly knows this (the NOTE comment and void makeApp;), but keeping ~10 lines of known-broken dead code in a regression-pinning suite is a trap for later maintainers. Either delete makeApp entirely, or fix it to mount the limiter before the routes (and use supertest) so the helper is actually valid. The direct-middleware runMiddleware approach is the right one; this leftover only adds confusion.

@Alajmah

Alajmah commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Adjudication — GitWire AI Review @ 0f3c412 (APPROVED, high confidence, 2 findings)

Disposition: none blocking; both accepted as recorded test-quality notes.

  1. [medium] fail-open test doesn't assert the logging its title pins — correct. The test pins the PASS-BEHAVIOR (request allowed) which is the security-relevant half; the logger.error call is asserted nowhere. The mocked logger exists in the file, so adding expect(logger.error).toHaveBeenCalled() is a one-line strengthening — recorded for the dismissal-gate round rather than churning the branch (the fail-open semantics themselves are what Unit C was authorized to pin, and they are pinned).

  2. [low] unused makeApp helper — correct, dead code. void makeApp marks it deliberately unused; deleting it is cleanup. It documents the production mount shape the middleware-direct tests intentionally bypass — if it were ever wired up as written (routes BEFORE limiter), its tests would indeed be vacuous, which is exactly why it is not wired up.

Gate state: all checks green, mergeStateStatus: CLEAN, head 0f3c412d3df2417a46e5fd5b2e279250cf319126 — holding for maintainer clearance.

@Alajmah

Alajmah commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by the v2 PR: logging assertion, TTL pin, mount-order fix, and the end-to-end non-Bearer identity-bypass reproducer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant