Skip to content

🛡️ Sentinel: dual rate-limiting to prevent IP/Account Lockout DoS - #113

Open
projectamazonph wants to merge 4 commits into
mainfrom
jules-13655340484834439284-919e693c
Open

🛡️ Sentinel: dual rate-limiting to prevent IP/Account Lockout DoS#113
projectamazonph wants to merge 4 commits into
mainfrom
jules-13655340484834439284-919e693c

Conversation

@projectamazonph

@projectamazonph projectamazonph commented Aug 6, 2026

Copy link
Copy Markdown
Owner

🚨 Severity: MEDIUM

💡 Vulnerability:

Previously, rate-limiting on signup and signin actions was strictly limited by the target email address. This enabled malicious actors to trigger rate limits for target accounts, causing legitimate users to suffer from Account Lockout Denial of Service (DoS) attacks. It also made the event-loop vulnerable to distributed brute-force or credential stuffing.

🔧 Fix:

  • Implemented an asynchronous dual rate-limiting function (rateLimitDual) in src/lib/rate-limit.ts that constraints both the client IP and target email.
  • Structured the utility to verify client IP limit first. If client IP exceeds rate limits, it bails out early and never updates the target email bucket, preventing account lockout pollution.
  • Updated signUpAction and signInAction in src/app/actions/auth.ts to use rateLimitDual.
  • Added a resetRateLimits() test reset function to prevent test pollution and mock state leak.

✅ Verification:

  • Added src/lib/__tests__/rate-limit.test.ts with 100% statement, branch, and function coverage.
  • All 219 unit/integration tests pass perfectly.
  • Clean type-checks (pnpm typecheck) and successful production build (pnpm build).
  • Documented key security learnings in .jules/sentinel.md.

PR created automatically by Jules for task 13655340484834439284 started by @projectamazonph

Summary by CodeRabbit

  • New Features

    • Added dual rate limiting using both client IP addresses and target-specific limits.
    • Blocked IP requests are rejected before consuming target-limit capacity.
    • Added support for clearing in-memory rate-limit state.
  • Bug Fixes

    • Improved protection for authentication sign-up and sign-in actions against repeated requests.
  • Tests

    • Expanded coverage for rate-limit enforcement, retry timing, IP detection, cleanup, and blocked-request behavior.

…and Account Lockout DoS (STORY-058)

- Added asynchronous dual rate-limiting utility rateLimitDual in src/lib/rate-limit.ts
- Updated signUpAction and signInAction to use rateLimitDual
- Updated next/headers mocks for vitest
- Added comprehensive unit tests in rate-limit.test.ts, achieving 100% coverage
- Logged critical learning in .jules/sentinel.md

Co-authored-by: projectamazonph <286085559+projectamazonph@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 12:59
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@projectamazonph, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ed7a010c-c8bb-4c91-ae80-b3d056c4a8dc

📥 Commits

Reviewing files that changed from the base of the PR and between 155eeff and 55d5d7d.

📒 Files selected for processing (2)
  • package.json
  • pnpm-workspace.yaml
📝 Walkthrough

Walkthrough

Authentication signup and signin now use IP-first dual rate limiting. The rate-limit module adds request-header handling and a bucket reset helper. Tests cover enforcement, cleanup, IP selection, and test isolation.

Changes

Authentication rate limiting

Layer / File(s) Summary
Dual limiter implementation
src/lib/rate-limit.ts, src/lib/__tests__/rate-limit.test.ts
Adds IP-first rateLimitDual, bucket reset support, and coverage for limits, retry timing, cleanup, headers, and blocked requests.
Authentication action integration
src/app/actions/auth.ts
Signup and signin now call rateLimitDual with the existing email keys and limits.
Test header mocks and isolation
src/__tests__/setup.ts, src/app/actions/__tests__/auth-actions.test.ts, .jules/sentinel.md
Mocks asynchronous next/headers access and resets in-memory rate-limit state before authentication tests. Documents the test pollution issue and reset recommendation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuthAction
  participant NextHeaders
  participant RateLimit
  AuthAction->>NextHeaders: Read client IP
  NextHeaders-->>AuthAction: Return header value
  AuthAction->>RateLimit: Check IP bucket
  RateLimit-->>AuthAction: Allow or reject
  AuthAction->>RateLimit: Check email bucket when allowed
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the PR's primary change: dual rate limiting to mitigate IP and account lockout denial-of-service attacks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jules-13655340484834439284-919e693c

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/lib/__tests__/rate-limit.test.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Colocate this Vitest file with its implementation.

Move this file to src/lib/rate-limit.test.ts. The coding guideline requires foo.test.ts next to foo.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/__tests__/rate-limit.test.ts` at line 1, Move the Vitest test file
from the __tests__ directory to sit alongside the rate-limit implementation as
src/lib/rate-limit.test.ts, preserving its existing test contents and behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/__tests__/rate-limit.test.ts`:
- Around line 1-17: Import resetRateLimits alongside rateLimit and
rateLimitDual, then invoke resetRateLimits in the existing beforeEach setup so
every test starts with isolated rate-limit bucket state.

In `@src/lib/rate-limit.ts`:
- Around line 61-63: Update the IP identity handling in the rate-limit flow so
missing or untrusted client-supplied headers do not share the ip:unknown bucket
or select another user's bucket; apply the IP limit only to trusted
ingress-provided identities, or use the existing per-target-first fallback with
its first hit counted. Adjust the tests covering the “unknown” fallback to
assert the selected behavior, and verify the deployment ingress replaces rather
than forwards client-controlled x-forwarded-for values.

---

Nitpick comments:
In `@src/lib/__tests__/rate-limit.test.ts`:
- Line 1: Move the Vitest test file from the __tests__ directory to sit
alongside the rate-limit implementation as src/lib/rate-limit.test.ts,
preserving its existing test contents and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 19dfd4f0-a02a-4839-bcc5-cc3d7041cc01

📥 Commits

Reviewing files that changed from the base of the PR and between 9d0e0bf and 155eeff.

📒 Files selected for processing (6)
  • .jules/sentinel.md
  • src/__tests__/setup.ts
  • src/app/actions/__tests__/auth-actions.test.ts
  • src/app/actions/auth.ts
  • src/lib/__tests__/rate-limit.test.ts
  • src/lib/rate-limit.ts

Comment on lines +1 to +17
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

const mockHeadersGet = vi.fn<(key: string) => string | null>(() => null);

vi.mock('next/headers', () => ({
headers: () => Promise.resolve({
get: (key: string) => mockHeadersGet(key),
}),
}));

import { rateLimit, rateLimitDual } from '../rate-limit';

describe('rate-limit.ts', () => {
beforeEach(() => {
mockHeadersGet.mockReset();
vi.useFakeTimers();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reset rate-limit state before each test.

buckets persists across test cases. Import resetRateLimits and call it in beforeEach so each test starts with isolated limiter state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/__tests__/rate-limit.test.ts` around lines 1 - 17, Import
resetRateLimits alongside rateLimit and rateLimitDual, then invoke
resetRateLimits in the existing beforeEach setup so every test starts with
isolated rate-limit bucket state.

Comment thread src/lib/rate-limit.ts
Comment on lines +61 to +63
const heads = await headers();
const xff = heads.get('x-forwarded-for');
const ip = (xff ? xff.split(',')[0]?.trim() : null) ?? heads.get('x-real-ip') ?? 'unknown';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect repository deployment configuration and documentation for forwarding-header handling.
fd -HI -t f . | rg '(next\.config|vercel\.json|Dockerfile|docker-compose|nginx|caddy|traefik|kubernetes|helm|README|docs)' \
  | while IFS= read -r file; do
      rg -n -i -C 3 'x-forwarded-for|x-real-ip|trusted proxy|proxy_set_header|real_ip_header' "$file" || true
    done

Repository: projectamazonph/amph-v2

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -HI -t f 'rate-limit|rate-limit\.test|vercel\.json|next\.config|Dockerfile|docker-compose|.*nginx.*|.*caddy.*|.*traefik.*|.*kubernetes.*|.*helm.*|README|docs/voice-guide' . \
  | sed -n '1,120p'

echo "== src/lib/rate-limit.ts outline =="
ast-grep outline src/lib/rate-limit.ts 2>/dev/null || true

echo "== src/lib/rate-limit.ts relevant lines =="
cat -n src/lib/rate-limit.ts | sed -n '1,150p'

echo "== src/lib/__tests__/rate-limit.test.ts relevant lines =="
cat -n src/lib/__tests__/rate-limit.test.ts | sed -n '1,220p'

echo "== search for use of limitRequest/rate limiting helpers =="
rg -n "limitRequest|rateLimit|rate-limit|unknown|x-forwarded-for|x-real-ip" src package.json -S

Repository: projectamazonph/amph-v2

Length of output: 31800


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -HI -t f 'rate-limit|rate-limit\.test|vercel\.json|next\.config|Dockerfile|docker-compose|.*nginx.*|.*caddy.*|.*traefik.*|.*kubernetes.*|.*helm.*|README|docs/voice-guide' . \
  | sed -n '1,120p'

echo "== rate-limit source excerpt =="
cat -n src/lib/rate-limit.ts | sed -n '1,150p'

echo "== rate-limit tests excerpt =="
cat -n src/lib/__tests__/rate-limit.test.ts | sed -n '1,220p'

echo "== usages/excerpts =="
rg -n "limitRequest|rateLimit|rate-limit|unknown|x-forwarded-for|x-real-ip" src package.json -S

Repository: projectamazonph/amph-v2

Length of output: 31357


Do not rate-limit missing-identity requests in a shared ip:unknown bucket.

When both x-forwarded-for and x-real-ip are absent, sign-in and sign-up requests use the same ip:unknown bucket. Five of those requests can block other authenticated sign-in/sign-up attempts to different targets for 60 seconds. Only apply the IP bucket for trusted ingress-supplied identities or add a separate per-target-first default path, with the first allowed hit counted; otherwise, skip the IP check for client-supplied/missing headers. Update the existing “falls back to unknown” tests to match the chosen default behavior.

Verify the deployment ingress replaces client-supplied x-forwarded-for; if it passes user-controlled values through, it can select the IP rate-limit bucket.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/rate-limit.ts` around lines 61 - 63, Update the IP identity handling
in the rate-limit flow so missing or untrusted client-supplied headers do not
share the ip:unknown bucket or select another user's bucket; apply the IP limit
only to trusted ingress-provided identities, or use the existing
per-target-first fallback with its first hit counted. Adjust the tests covering
the “unknown” fallback to assert the selected behavior, and verify the
deployment ingress replaces rather than forwards client-controlled
x-forwarded-for values.

google-labs-jules Bot and others added 3 commits August 6, 2026 13:07
…ix broken CI pnpm version (STORY-058)

- Added asynchronous dual rate-limiting utility rateLimitDual in src/lib/rate-limit.ts
- Updated signUpAction and signInAction to use rateLimitDual
- Updated next/headers mocks for vitest
- Added comprehensive unit tests in rate-limit.test.ts, achieving 100% coverage
- Logged critical learning in .jules/sentinel.md
- Changed packageManager in package.json to pnpm@11.12.0 to resolve broken pnpm v11.13.0 release in GitHub CI environment

Co-authored-by: projectamazonph <286085559+projectamazonph@users.noreply.github.com>
…ix broken CI pnpm version (STORY-058)

- Added asynchronous dual rate-limiting utility rateLimitDual in src/lib/rate-limit.ts
- Updated signUpAction and signInAction to use rateLimitDual
- Updated next/headers mocks for vitest
- Added comprehensive unit tests in rate-limit.test.ts, achieving 100% coverage
- Logged critical learning in .jules/sentinel.md
- Changed packageManager in package.json to pnpm@9.15.4 to resolve broken pnpm v11.x release in GitHub CI environment

Co-authored-by: projectamazonph <286085559+projectamazonph@users.noreply.github.com>
…ix broken CI pnpm version (STORY-058)

- Added asynchronous dual rate-limiting utility rateLimitDual in src/lib/rate-limit.ts
- Updated signUpAction and signInAction to use rateLimitDual
- Updated next/headers mocks for vitest
- Added comprehensive unit tests in rate-limit.test.ts, achieving 100% coverage
- Logged critical learning in .jules/sentinel.md
- Changed packageManager in package.json to pnpm@9.15.4 to resolve broken pnpm v11.x release in GitHub CI environment
- Added packages field to pnpm-workspace.yaml to resolve pnpm workspace validation errors in CI cache and setup-node commands

Co-authored-by: projectamazonph <286085559+projectamazonph@users.noreply.github.com>
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.

2 participants