-
Notifications
You must be signed in to change notification settings - Fork 0
🔒 Fix IP Spoofing vulnerability in rate limiter #304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,3 +1,4 @@ | ||||||
| import { NextRequest } from "next/server"; | ||||||
| import { RateLimiter } from "@/lib/rateLimit"; | ||||||
| import { fetchCardData } from "@/lib/cardDataFetcher"; | ||||||
| import { parseCardQueryParams, renderCardResponse, renderErrorCardResponse } from "@/lib/cardRenderer"; | ||||||
|
|
@@ -10,7 +11,7 @@ const SUCCESS_CACHE = "public, s-maxage=1800, stale-while-revalidate=3600"; | |||||
| const ERROR_CACHE = "public, s-maxage=60, stale-while-revalidate=120"; | ||||||
|
|
||||||
| export async function GET( | ||||||
| request: Request, | ||||||
| request: NextRequest, | ||||||
| { params }: { params: Promise<{ username: string }> } | ||||||
| ): Promise<Response> { | ||||||
| const { username } = await params; | ||||||
|
|
@@ -19,7 +20,7 @@ export async function GET( | |||||
| const allowedOrigin = process.env.APP_URL || "http://localhost:3000"; | ||||||
| const fontUrl = `${allowedOrigin}/fonts/NotoSans-Regular.ttf`; | ||||||
|
|
||||||
| const ip = request.headers.get("x-forwarded-for") ?? "unknown"; | ||||||
| const ip = request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0] ?? "unknown"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/app/api/card/[username]/route.ts
Line: 23
Comment:
**`x-forwarded-for` フォールバックが依然としてなりすまし可能**
`x-real-ip` が存在しない場合(ローカル開発・Vercel 以外のデプロイ環境など)、フォールバックとして `x-forwarded-for` の **最初(左端)の値** が使われますが、これはクライアントが任意に設定できるヘッダーです。Vercel の Edge Network はクライアントが付与した `x-forwarded-for` 値をそのまま連結して転送するため、攻撃者が `X-Forwarded-For: spoofed-ip` を送ると `split(",")[0]` は `spoofed-ip` を返し、レート制限を簡単に回避できます。フォールバックには左端(クライアント側)ではなく **右端(信頼できるプロキシが付加した)** の IP を使うか、`x-real-ip` がなければ `"unknown"` に固定することを検討してください。同じ問題が `src/app/api/og/[username]/route.tsx` の 20 行目にも存在します。
How can I resolve this? If you propose a fix, please make it concise.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: src/app/api/card/[username]/route.ts
Line: 23
Comment:
**`x-forwarded-for` の最初の IP に `.trim()` がない**
`"1.2.3.4, 5.6.7.8"` のようにカンマ後にスペースが入るフォーマットでは最初の値に空白は付きませんが、プロキシによっては先頭にスペースを付与することがあります。旧 OG ルートは `.trim()` を呼び出していたため、同様に追加しておくと安全です。
```suggestion
const ip = request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown";
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||
| const rateLimitResult = rateLimiter.check(ip); | ||||||
|
|
||||||
| if (!rateLimitResult.success) { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -17,8 +17,7 @@ | |||||
| ) { | ||||||
| const { username } = await params; | ||||||
|
|
||||||
| const forwarded = request.headers.get("x-forwarded-for"); | ||||||
| const ip = forwarded ? forwarded.split(",").at(-1)?.trim() ?? "unknown" : "unknown"; | ||||||
| const ip = request.headers.get("x-real-ip") ?? request.headers.get("x-forwarded-for")?.split(",")[0] ?? "unknown"; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the
Suggested change
References
|
||||||
| const rateLimitResult = rateLimiter.check(ip); | ||||||
|
|
||||||
| if (!rateLimitResult.success) { | ||||||
|
|
@@ -84,7 +83,7 @@ | |||||
| }} | ||||||
| > | ||||||
| {avatarUrl && ( | ||||||
| <img | ||||||
|
Check warning on line 86 in src/app/api/og/[username]/route.tsx
|
||||||
| src={sanitizeUrl(avatarUrl)} | ||||||
| alt="" | ||||||
| width={120} | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
request.ipis the recommended and more secure way to retrieve the client's IP address in Next.js, especially when using the Edge runtime. Manually parsing thex-forwarded-forheader by taking the first element (split(",")[0]) is susceptible to IP spoofing, as an attacker can easily prepend arbitrary IP addresses to this header. Furthermore, the current implementation lacks.trim(), which could lead to inconsistent rate limiting keys if the header contains whitespace.Additionally, consider centralizing this IP extraction logic into a utility function to maintain consistency across different API routes and reduce duplication, as suggested by the general rules. Note that if you switch to
request.ip, you may need to update your tests to mock theipproperty on theNextRequestobject (e.g., usingObject.defineProperty).References