Skip to content

feat: implement reusable rate limiting middleware#99

Open
pari-28 wants to merge 2 commits into
rushikesh-bobade:mainfrom
pari-28:issue-74-rate-limit
Open

feat: implement reusable rate limiting middleware#99
pari-28 wants to merge 2 commits into
rushikesh-bobade:mainfrom
pari-28:issue-74-rate-limit

Conversation

@pari-28

@pari-28 pari-28 commented Jul 4, 2026

Copy link
Copy Markdown

Description

This PR introduces a reusable backend rate-limiting utility to improve security against brute-force attacks and excessive API requests.

Changes made

  • Added a reusable rateLimit(request, limit, windowMs) utility in app/utils/rate-limit.server.ts
  • Applied rate limiting to:
    • Login route (login-page.tsx)
    • Signup route (signup-page.tsx)
    • AI Insights API (api.insights.ts)
  • Returns HTTP 429 Too Many Requests with a clear error message when the request limit is exceeded
  • Updated CONTRIBUTING.md with usage instructions and documented the limitations of the in-memory implementation for serverless environments.

Notes

Related Issues

Fixes #74

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings

Screenshots (if applicable)

N/A (Backend changes only)

@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

@pari-28 is attempting to deploy a commit to the participationcorner2025-8967's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the ECSoC26 Required label for ECSOC Sentinel scoring label Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown

📝 Summary

The pull request introduces a rate limiting feature to prevent excessive requests from a single IP address. It adds a new utility function rateLimit in app/utils/rate-limit.server.ts and applies it to several routes, including api.insights, login-page, and signup-page.

📂 Files Changed

  • CONTRIBUTING.md: Added documentation for the rate limiting feature
  • app/routes/api.insights.ts: Applied rate limiting to the action function
  • app/routes/login-page.tsx: Applied rate limiting to the action function
  • app/routes/signup-page.tsx: Applied rate limiting to the action function
  • app/utils/rate-limit.server.ts: New file containing the rateLimit utility function

🎭 Code Poem

Rate limiting is now in place,
To prevent abuse and excessive pace,
With a new utility function so fine,
Rate limiting is now truly divine.

🚨 Bugs & Architectural Violations * The `rateLimit` function uses an in-memory store, which may not be suitable for production deployments with multiple server instances. Consider using a shared backend like Upstash Redis or Vercel KV. * The `rateLimit` function does not handle errors well. If an error occurs while checking the rate limit, it will be swallowed and not propagated to the caller. * The `getClientIp` function assumes that the `x-forwarded-for` header is always present and correctly formatted. Consider adding more robust handling for this header. * The `rateLimit` function does not provide any logging or monitoring capabilities. Consider adding logging statements to track rate limit events. * No accessibility features are added or modified in this pull request, but it's essential to ensure that any new features or changes do not introduce accessibility issues.
💡 Suggestions & Best Practices * Consider adding more robust error handling to the `rateLimit` function, including logging and propagation of errors to the caller. * Add logging statements to track rate limit events, such as when a request is blocked due to exceeding the rate limit. * Consider using a more robust IP address parsing library instead of relying on the `x-forwarded-for` header. * Add documentation for the `rateLimit` function, including its parameters, return values, and any exceptions it may throw. * Consider adding a `defer` statement to the `rateLimit` function to ensure that it does not block the main thread. * Review the code for any potential performance issues, such as excessive memory allocation or computationally expensive operations.

@rushikesh-bobade

Copy link
Copy Markdown
Owner

@pari-28 Great initiative taking on security! The implementation logic itself is very clean, but there is a critical architectural blocker we need to fix before merging.

  1. Serverless State (Critical): You implemented the rate limit store using an in-memory Javascript Map(). Because FlipTrack is deployed on Vercel (which uses stateless Serverless Functions), memory is not shared across concurrent requests. If an attacker floods the API, Vercel will spin up dozens of isolated serverless instances, each with their own empty Map(), completely bypassing your rate limit.
    Fix: You must persist the rate limit data in a shared store. Since we already use Prisma, please create a new RateLimit model in schema.prisma (with fields for IP, count, and reset timestamp) and perform the count checks against the database instead of memory.
  2. IP Spoofing: Be careful with trusting the first value of x-forwarded-for, as attackers can easily spoof this header to bypass the limit. Please ensure the IP extraction logic is robust against spoofing in a cloud environment.

Could you update the implementation to use Prisma for the storage? Let me know if you need any pointers on designing the schema for it!

@pari-28

pari-28 commented Jul 6, 2026

Copy link
Copy Markdown
Author

Hi @rushikesh-bobade, thank you for the detailed review!

I'm updating the implementation to use a Prisma-backed RateLimit model as suggested. Before proceeding, I wanted to clarify the expected implementation approach.

Should the RateLimit table be managed entirely through Prisma migrations using a local development database, or is there a preferred workflow for contributors when making schema changes in this repository?

Also, if you have any recommendations for the RateLimit schema design or the IP handling approach you'd like followed, I'd be happy to align the implementation with the project's expectations.

Thanks!

@rushikesh-bobade

Copy link
Copy Markdown
Owner

Hi @pari-28! Great questions.

1. Prisma Migration Workflow

We actually just merged a massive infrastructure update (PR #110) specifically to make this easier for contributors!

You can now manage the database locally:

  1. Make sure you pull the latest main branch into your branch.
  2. Run docker-compose up -d to spin up your own local PostgreSQL database.
  3. Check the updated .env.example file and copy the local Docker DATABASE_URL into your .env file.
  4. Add the RateLimit model to schema.prisma.
  5. Run npx prisma migrate dev --name add-rate-limit-model to generate the migration file, and commit that file to this PR!

2. Suggested Schema Design

Here is a lightweight, performant schema you can add to the bottom of prisma/schema.prisma:

model RateLimit {
  ip          String   @id
  count       Int      @default(1)
  resetAt     DateTime
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

Note: We use the ip as the @id primary key for extremely fast lookups.

3. Handling IPs Securely on Vercel

Because we host on Vercel, the safest way to extract the true client IP (and prevent x-forwarded-for spoofing) is to check Vercel's proprietary header first, then fallback to standard headers. You can use this utility:

export function getClientIp(request: Request) {
  return request.headers.get("x-vercel-forwarded-for") || 
         request.headers.get("x-real-ip") || 
         request.headers.get("x-forwarded-for")?.split(",")[0].trim() || 
         "unknown_ip";
}

Let me know if you run into any issues spinning up the local Docker database!

@pari-28 pari-28 force-pushed the issue-74-rate-limit branch from beea7f1 to deaa5be Compare July 8, 2026 16:43
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

📝 Summary

The provided pull request introduces a rate limiting feature to prevent excessive requests from a single IP address. It adds a new RateLimit model to the Prisma schema, creates a new table in the database, and implements a rateLimit function in app/utils/rate-limit.server.ts. This function checks the request's IP address and limits the number of requests within a specified time window.

📂 Files Changed

  • CONTRIBUTING.md: Added documentation for the rate limiting feature.
  • app/routes/api.insights.ts: Imported and used the rateLimit function.
  • app/routes/login-page.tsx: Imported and used the rateLimit function.
  • app/routes/signup-page.tsx: Imported and used the rateLimit function.
  • app/utils/rate-limit.server.ts: New file containing the rateLimit function and getClientIp helper function.
  • prisma/migrations/20260708160733_add_rate_limit_model/migration.sql: New migration file creating the RateLimit table.
  • prisma/schema.prisma: Updated to include the new RateLimit model.

🎭 Code Poem

Rate limiting's in place now,
To prevent abuse, somehow.
With Prisma's help, it's quite neat,
And keeps our app's performance sweet.

🚨 Bugs & Architectural Violations The provided code seems to follow the architectural rules, but there are a few potential issues: * The `rateLimit` function uses an in-memory store, which may not be suitable for a distributed environment. As mentioned in the documentation, a shared backend like Upstash Redis or Vercel KV should be considered for production deployments. * The `getClientIp` function relies on specific headers to determine the client's IP address. This might not work in all scenarios, especially if the request is proxied or load-balanced. * There is no error handling for the `rateLimit` function. If an error occurs while checking or updating the rate limit, it will not be caught or propagated. * The `RateLimit` model has a default value of 1 for the `count` field, but it's not clear why this is the case. It might be better to use a more explicit value or to calculate the initial count based on the request's timestamp. Looks solid in terms of architectural rules, but these potential issues should be addressed.
💡 Suggestions & Best Practices Some suggestions for improving the code: * Consider using a more robust IP address detection mechanism, such as using a library like `ip-address` or `geoip-lite`. * Add error handling for the `rateLimit` function to ensure that any errors are caught and propagated correctly. * Use a more explicit value for the `count` field in the `RateLimit` model, or calculate the initial count based on the request's timestamp. * Consider adding a `max` field to the `RateLimit` model to store the maximum allowed requests within the time window. * Use a more secure way to store the `accessToken` and `refreshToken` fields in the `Integration` model, such as using a secure encryption mechanism. * Consider adding a `ttl` (time to live) field to the `RateLimit` model to automatically remove expired rate limits.

@pari-28

pari-28 commented Jul 8, 2026

Copy link
Copy Markdown
Author

Hi @rushikesh-bobade,

I've addressed the requested changes:

  • Replaced the in-memory Map with a Prisma-backed RateLimit implementation.
  • Added the RateLimit model and generated the Prisma migration using the local Docker PostgreSQL workflow.
  • Updated the client IP extraction to use the recommended Vercel-first header order.
  • Verified the project passes npm run typecheck.

Please let me know if you'd like me to make any further refinements. Thanks for the guidance!

@rushikesh-bobade

rushikesh-bobade commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Hi @pari-28,

Amazing job adapting to the Prisma schema! The migration file is perfect, the getClientIp logic is exactly what we need for Vercel, and the entire project typechecks cleanly with your changes.

However, during my line-by-line review, I spotted a critical security vulnerability in the rate limit logic: A TOCTOU (Time-Of-Check to Time-Of-Use) Race Condition.

Currently, you are calling findUnique, checking the count in javascript memory, and then calling update. If an attacker sends 100 requests at the exact same millisecond, all 100 requests will read the old count simultaneously and bypass the limit check entirely!

The Fix:
You must use an atomic database operation so the check and increment happen in a single step. You can achieve this using Prisma's upsert combined with increment: 1:

  const now = new Date();
  const resetAt = new Date(now.getTime() + windowMs);

  // 1. Atomically increment the count (or create if it doesn't exist)
  const result = await prisma.rateLimit.upsert({
    where: { ip },
    create: { ip, count: 1, resetAt },
    update: { count: { increment: 1 } },
  });

  // 2. If the reset period has expired, reset it back to 1
  if (result.resetAt <= now) {
    await prisma.rateLimit.update({
      where: { ip },
      data: { count: 1, resetAt },
    });
    return;
  }

  // 3. Now safely check the atomic result
  if (result.count > limit) {
    throw new Response(
      JSON.stringify({ error: "Too many attempts. Please try again later." }),
      { status: 429, headers: { "Content-Type": "application/json" } }
    );
  }
  

Could you update rate-limit.server.ts with this atomic upsert pattern? Once you push that fix, this is good to merge! Great work on this so far.

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

Labels

ECSoC26 Required label for ECSOC Sentinel scoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: Implement Rate Limiting on Auth and API Routes

2 participants