Skip to content

🛡️ Sentinel: implement dual rate limiting for authentication actions - #124

Open
projectamazonph wants to merge 2 commits into
mainfrom
jules-18249317339137429242-cc4416b1
Open

🛡️ Sentinel: implement dual rate limiting for authentication actions#124
projectamazonph wants to merge 2 commits into
mainfrom
jules-18249317339137429242-cc4416b1

Conversation

@projectamazonph

@projectamazonph projectamazonph commented Aug 11, 2026

Copy link
Copy Markdown
Owner

🚨 Severity

MEDIUM / SECURITY ENHANCEMENT

💡 Vulnerability

Simple single rate limiting based purely on user-specific identifiers (like target emails) can be abused by malicious actors to lock legitimate users out of their accounts (Account Lockout DoS).

🎯 Impact

Without dual rate limiting, an attacker can continuously send bad login/signup attempts for a victim's email, triggering the target rate limiter and blocking the legitimate user from logging in or registering.

🔧 Fix

  1. Implemented rateLimitDual in src/lib/rate-limit.ts to check client IP-based rate limits before checking target-based keys (such as email). This ensures malicious IPs are blocked first, protecting the target's bucket from exhaustion.
  2. Handled standard reverse proxy headers (x-forwarded-for and x-real-ip) safely for IP extraction, considering noUncheckedIndexedAccess type safety.
  3. Updated signUpAction and signInAction in src/app/actions/auth.ts to use rateLimitDual asynchronously.
  4. Added an in-memory reset utility resetRateLimits() to prevent test pollution.
  5. Added highly detailed unit tests in src/lib/__tests__/rate-limit.test.ts to verify sliding-window behavior, fallback IPs, and dual execution order.

✅ Verification

  • All 219 unit tests pass successfully.
  • Typecheck and ESLint checks pass cleanly.

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved sign-in and sign-up protection against abuse by applying limits based on both client IP address and account identifier.
    • Helps prevent attempts to lock out accounts through repeated requests from a single source.
  • Tests

    • Added coverage for rate-limit thresholds, expiration, client IP detection, fallback behavior, and cleanup.

…nt lockout DoS

Co-authored-by: projectamazonph <286085559+projectamazonph@users.noreply.github.com>
@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 lite review requested due to automatic review settings August 11, 2026 12:50

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 11, 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: 54 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: 73a8c545-e2ce-47b0-9203-327b507f6b9e

📥 Commits

Reviewing files that changed from the base of the PR and between a43145f and 017dc36.

📒 Files selected for processing (1)
  • package.json
📝 Walkthrough

Walkthrough

Authentication signup and signin actions now use IP-first dual rate limiting. The limiter extracts client IPs from request headers, enforces separate IP and target limits, and includes expanded tests.

Changes

Dual rate limiting

Layer / File(s) Summary
Rate limiter behavior
src/lib/rate-limit.ts, src/lib/__tests__/rate-limit.test.ts, .jules/sentinel.md
The limiter extracts client IPs from forwarded or real-IP headers, checks IP limits before lowercased target limits, supports state reset, and includes coverage for enforcement, expiration, cleanup, and fallbacks.
Authentication action integration
src/app/actions/auth.ts, src/app/actions/__tests__/auth-actions.test.ts
Signup and signin use rateLimitDual with IP and target limits of 10 and 5 requests per 60 seconds. The header mock now provides asynchronous header access.

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

Sequence Diagram(s)

sequenceDiagram
  participant AuthAction
  participant rateLimitDual
  participant RequestHeaders
  participant RateLimitBuckets

  AuthAction->>rateLimitDual: Check signup or signin target
  rateLimitDual->>RequestHeaders: Read client IP headers
  RequestHeaders-->>rateLimitDual: Return client IP or unknown
  rateLimitDual->>RateLimitBuckets: Check IP limit
  RateLimitBuckets-->>rateLimitDual: Allow or reject
  rateLimitDual->>RateLimitBuckets: Check target limit when IP is allowed
  RateLimitBuckets-->>rateLimitDual: Allow or reject
  rateLimitDual-->>AuthAction: Return rate-limit result
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 implementation of dual rate limiting for authentication actions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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-18249317339137429242-cc4416b1

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: 3

🤖 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/app/actions/auth.ts`:
- Line 35: Set the signup rate-limit configuration at src/app/actions/auth.ts
lines 35-35 so the IP quota is lower than the target quota, or adjust the
target-bucket policy accordingly; apply the same safe relationship at
src/app/actions/auth.ts lines 128-128 for signin. Update .jules/sentinel.md
lines 3-6 to state that IP-first ordering reduces lockout risk only when the IP
quota blocks the source before the target quota is exhausted.

In `@src/lib/__tests__/rate-limit.test.ts`:
- Around line 61-120: The rateLimitDual tests lack coverage proving target keys
are normalized consistently. Add a test in the rateLimitDual suite that calls
rateLimitDual with the same target using different casing and verifies the
second call is rejected or otherwise shares the first call’s target bucket under
a target limit of one.

In `@src/lib/rate-limit.ts`:
- Around line 66-70: The rateLimitDual client-IP selection currently assigns
headerless requests to a shared unknown bucket. Update rateLimitDual to use a
platform-provided or validated client IP and skip IP-based limiting when no
trusted address is available; preserve other rate-limit behavior. In
src/lib/__tests__/rate-limit.test.ts lines 114-119, add a regression test
covering repeated headerless requests and verifying they are not blocked by the
shared IP bucket.
🪄 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: 8893f307-109f-41b7-a3db-85c8eeea8509

📥 Commits

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

📒 Files selected for processing (5)
  • .jules/sentinel.md
  • 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 thread src/app/actions/auth.ts

export const signUpAction = createSafeAction(signUpSchema, async (data) => {
const rl = rateLimit(`signup:${data.email.toLowerCase()}`, 5, 60_000);
const rl = await rateLimitDual('signup', data.email, 10, 5, 60_000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Set the IP limit below the target limit.

With ipLimit = 10 and targetLimit = 5, one IP can submit five requests for a victim email and exhaust its target bucket before the IP bucket blocks it. IP-first ordering does not prevent this lockout.

  • src/app/actions/auth.ts#L35-L35: set the signup IP limit below the target limit, or change the target-bucket policy.
  • src/app/actions/auth.ts#L128-L128: apply the same safe limit relationship to signin.
  • .jules/sentinel.md#L3-L6: state that IP-first ordering reduces lockout risk only when the IP quota blocks the source before it exhausts the target quota.
📍 Affects 2 files
  • src/app/actions/auth.ts#L35-L35 (this comment)
  • src/app/actions/auth.ts#L128-L128
  • .jules/sentinel.md#L3-L6
🤖 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/app/actions/auth.ts` at line 35, Set the signup rate-limit configuration
at src/app/actions/auth.ts lines 35-35 so the IP quota is lower than the target
quota, or adjust the target-bucket policy accordingly; apply the same safe
relationship at src/app/actions/auth.ts lines 128-128 for signin. Update
.jules/sentinel.md lines 3-6 to state that IP-first ordering reduces lockout
risk only when the IP quota blocks the source before the target quota is
exhausted.

Comment on lines +61 to +120
describe('rateLimitDual', () => {
it('uses x-forwarded-for header for client IP rate limiting', async () => {
mockHeadersGet.mockImplementation((header: string) => {
if (header === 'x-forwarded-for') return '1.2.3.4, 5.6.7.8';
return null;
});

// Under IP limit of 2, target limit of 1
// First attempt: should succeed for both IP and target
const res1 = await rateLimitDual('login', 'user@example.com', 2, 1, 60_000);
expect(res1.allowed).toBe(true);

// Second attempt with same email: target limit exceeded (limit is 1)
const res2 = await rateLimitDual('login', 'user@example.com', 2, 1, 60_000);
expect(res2.allowed).toBe(false);
});

it('IP limit check runs BEFORE target check to prevent account lockout', async () => {
mockHeadersGet.mockImplementation((header: string) => {
if (header === 'x-forwarded-for') return '9.9.9.9';
return null;
});

// IP limit = 1, target limit = 5
// First attempt: allowed
const res1 = await rateLimitDual('login', 'legit@example.com', 1, 5, 60_000);
expect(res1.allowed).toBe(true);

// Second attempt: IP blocked (since IP limit is 1)
const res2 = await rateLimitDual('login', 'legit@example.com', 1, 5, 60_000);
expect(res2.allowed).toBe(false);

// Verify target-based key did not get polluted by checking another IP
mockHeadersGet.mockImplementation((header: string) => {
if (header === 'x-forwarded-for') return '8.8.8.8'; // safe IP
return null;
});

// Legit user on safe IP can still log in because their target bucket wasn't polluted by blocked IP
const res3 = await rateLimitDual('login', 'legit@example.com', 1, 5, 60_000);
expect(res3.allowed).toBe(true);
});

it('falls back to x-real-ip when x-forwarded-for is missing', async () => {
mockHeadersGet.mockImplementation((header: string) => {
if (header === 'x-real-ip') return '127.0.0.1';
return null;
});

const res = await rateLimitDual('login', 'user@example.com', 1, 1, 60_000);
expect(res.allowed).toBe(true);
});

it('falls back to unknown when both headers are missing', async () => {
mockHeadersGet.mockReturnValue(null);

const res = await rateLimitDual('login', 'user@example.com', 1, 1, 60_000);
expect(res.allowed).toBe(true);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a target-key normalization regression test.

Call rateLimitDual with the same target in mixed case. Assert that the second call reaches the same target bucket.

As per coding guidelines, “New features must include tests.”

🤖 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 61 - 120, The
rateLimitDual tests lack coverage proving target keys are normalized
consistently. Add a test in the rateLimitDual suite that calls rateLimitDual
with the same target using different casing and verifies the second call is
rejected or otherwise shares the first call’s target bucket under a target limit
of one.

Source: Coding guidelines

Comment thread src/lib/rate-limit.ts
Comment on lines +66 to +70
const xff = reqHeaders.get('x-forwarded-for');
const xri = reqHeaders.get('x-real-ip');

// Safely extract client IP from x-forwarded-for (handling noUncheckedIndexedAccess)
const clientIp = (xff ? xff.split(',')[0]?.trim() : null) || xri || '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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -i -C 3 \
  'x-forwarded-for|x-real-ip|trust proxy|remoteAddress|forwarded' \
  -g '!node_modules/**' -g '!**/.next/**' .

Repository: projectamazonph/amph-v2

Length of output: 4376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- rate-limit structure ---'
ast-grep outline src/lib/rate-limit.ts --view expanded
echo '--- rate-limit implementation ---'
cat -n src/lib/rate-limit.ts | sed -n '1,150p'

echo '--- rate-limit test structure and relevant tests ---'
ast-grep outline src/lib/__tests__/rate-limit.test.ts --view expanded
cat -n src/lib/__tests__/rate-limit.test.ts | sed -n '1,145p'

echo '--- rateLimitDual call sites ---'
rg -n -C 5 'rateLimitDual|rateLimit\(' src

Repository: projectamazonph/amph-v2

Length of output: 22721


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- all proxy and client-address references ---'
rg -n -i -C 3 \
  'x-forwarded-for|x-real-ip|forwarded|remoteAddress|trusted.?proxy|proxy.?trust|vercel|cloudflare|load.?balancer|ingress' \
  --glob '!node_modules/**' --glob '!**/.next/**' .

echo '--- deployment and framework configuration files ---'
git ls-files | rg '(^|/)(vercel\.json|next\.config\..*|middleware\..*|package\.json|Dockerfile.*|docker-compose.*|README.*|.*security.*|.*audit.*)$' | sort

echo '--- deterministic model of the headerless path ---'
python3 - <<'PY'
from collections import defaultdict

def simulate(requests: int, ip_limit: int, target_limit: int):
    buckets = defaultdict(int)
    results = []
    for _ in range(requests):
        ip_key = "ip:login:unknown"
        if buckets[ip_key] >= ip_limit:
            results.append(False)
            continue
        buckets[ip_key] += 1
        target_key = "target:login:user@example.com"
        if buckets[target_key] >= target_limit:
            results.append(False)
            continue
        buckets[target_key] += 1
        results.append(True)
    return results, dict(buckets)

results, buckets = simulate(11, 10, 100)
print({"allowed_count": sum(results), "request_10": results[9], "request_11": results[10], "buckets": buckets})
PY

Repository: projectamazonph/amph-v2

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- all proxy and client-address references ---'
rg -n -i -C 3 \
  'x-forwarded-for|x-real-ip|forwarded|remoteAddress|trusted.?proxy|proxy.?trust|vercel|cloudflare|load.?balancer|ingress' \
  --glob '!node_modules/**' --glob '!**/.next/**' .

echo '--- deployment and framework configuration files ---'
git ls-files | rg '(^|/)(vercel\.json|next\.config\..*|middleware\..*|package\.json|Dockerfile.*|docker-compose.*|README.*|.*security.*|.*audit.*)$' | sort

echo '--- deterministic model of the headerless path ---'
python3 - <<'PY'
from collections import defaultdict

def simulate(requests: int, ip_limit: int, target_limit: int):
    buckets = defaultdict(int)
    results = []
    for _ in range(requests):
        ip_key = "ip:login:unknown"
        if buckets[ip_key] >= ip_limit:
            results.append(False)
            continue
        buckets[ip_key] += 1
        target_key = "target:login:user@example.com"
        if buckets[target_key] >= target_limit:
            results.append(False)
            continue
        target_key = "target:login:user@example.com"
        if buckets[target_key] >= target_limit:
            results.append(False)
            continue
        target_key = "target:login:user@example.com"
        buckets[target_key] += 1
        results.append(True)
    return results, dict(buckets)

results, buckets = simulate(11, 10, 100)
print({"allowed_count": sum(results), "request_10": results[9], "request_11": results[10], "buckets": buckets})
PY

Repository: projectamazonph/amph-v2

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- exact deployment/framework files ---'
for f in $(git ls-files | rg '(^|/)(vercel\.json|next\.config\..*|middleware\..*|Dockerfile.*|docker-compose.*)$'); do
  echo "### $f"
  cat -n "$f"
done

echo '--- focused proxy references outside generated/lock/status files ---'
rg -n -i \
  'x-forwarded-for|x-real-ip|forwarded|remoteAddress|trusted.?proxy|proxy.?trust|client.?ip' \
  src next.config.ts middleware.ts middleware.js vercel.json 2>/dev/null || true

echo '--- deterministic limiter model ---'
python3 - <<'PY'
from collections import defaultdict

def rate_limit(buckets, key, limit):
    if buckets[key] >= limit:
        return False
    buckets[key] += 1
    return True

buckets = defaultdict(int)
results = []
for _ in range(11):
    if not rate_limit(buckets, "ip:signin:unknown", 10):
        results.append(False)
        continue
    results.append(rate_limit(buckets, "target:signin:user@example.com", 100))

print("allowed requests:", sum(results))
print("request 10 allowed:", results[9])
print("request 11 allowed:", results[10])
print("unknown IP hits:", buckets["ip:signin:unknown"])
PY

Repository: projectamazonph/amph-v2

Length of output: 11719


🌐 Web query:

Vercel documentation x-forwarded-for x-real-ip client supplied spoofing trusted IP headers

💡 Result:

Vercel takes specific measures to prevent IP spoofing by managing request headers like X-Forwarded-For and X-Real-IP [1]. Key points regarding Vercel's handling of these headers include: 1. Automatic Overwriting: To prevent IP spoofing, Vercel overwrites the standard X-Forwarded-For header for requests processed by its platform [1]. Consequently, you cannot rely on this header to pass external, client-supplied IP addresses directly to a Vercel deployment, as they will be replaced by the public IP address of the client as seen by Vercel's network [1]. 2. Equivalent Headers: The X-Real-IP header is treated as identical to X-Forwarded-For and is similarly managed by Vercel [1]. Additionally, Vercel provides x-vercel-forwarded-for, which contains the same information but is specifically intended for use in environments where you might need to distinguish the original IP from headers that could be overwritten by upstream proxies [1]. 3. Reverse Proxies and Verified Proxy: Vercel generally discourages placing a reverse proxy in front of a Vercel project because it obscures the end-user IP and reduces the effectiveness of Vercel's firewall [2]. However, if you must use a reverse proxy, Vercel offers a feature called Verified Proxy [3][4]. This system allows Vercel to trust specific headers (such as CF-Connecting-IP, True-Client-IP, or X-GCP-Connecting-IP) passed from known, verified providers to correctly identify the real client IP [3][4]. 4. Security Implications: Because client-supplied X-Forwarded-For headers are untrusted, any application logic relying on these headers for security, rate limiting, or access control is inherently vulnerable to spoofing unless handled through the Verified Proxy configuration for known, static infrastructure [2][3]. For applications requiring access to the original client IP, you should use the headers provided by Vercel's network or ensure your infrastructure is correctly onboarded to the Verified Proxy program if using an external load balancer or CDN [3][4].

Citations:


Avoid the shared unknown IP bucket.

When both headers are absent, rateLimitDual blocks all headerless clients after ten requests in the same instance. Use a platform-provided or validated client IP, and skip the IP bucket when no trusted address exists. Add a repeated headerless-request regression test.

📍 Affects 2 files
  • src/lib/rate-limit.ts#L66-L70 (this comment)
  • src/lib/__tests__/rate-limit.test.ts#L114-L119
🤖 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 66 - 70, The rateLimitDual client-IP
selection currently assigns headerless requests to a shared unknown bucket.
Update rateLimitDual to use a platform-provided or validated client IP and skip
IP-based limiting when no trusted address is available; preserve other
rate-limit behavior. In src/lib/__tests__/rate-limit.test.ts lines 114-119, add
a regression test covering repeated headerless requests and verifying they are
not blocked by the shared IP bucket.

…nt lockout DoS

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