-
Notifications
You must be signed in to change notification settings - Fork 0
🔒 Fix Insecure Randomness in IP Fallback Check #375
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
base: main
Are you sure you want to change the base?
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -97,13 +97,13 @@ describe("RateLimiter", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("getClientIp", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns the right-most x-forwarded-for IP", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns the left-most x-forwarded-for IP", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": "5.6.7.8, 9.10.11.12" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("9.10.11.12"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("5.6.7.8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("trims whitespace from the selected x-forwarded-for IP", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -112,7 +112,7 @@ describe("getClientIp", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": " 5.6.7.8 , 9.10.11.12 " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("9.10.11.12"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("5.6.7.8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("accepts IPv6 x-forwarded-for values", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -124,13 +124,13 @@ describe("getClientIp", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("2001:db8::1"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("does not trust x-real-ip when x-forwarded-for is absent", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("trusts x-real-ip when available", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-real-ip": "1.2.3.4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("unknown"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("1.2.3.4"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns unknown if neither header is present", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -147,21 +147,50 @@ describe("getClientIp", () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("unknown"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns unknown when the right-most x-forwarded-for token is empty", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns unknown when the left-most x-forwarded-for token is empty", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": "5.6.7.8, " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": " , 5.6.7.8" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("unknown"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns unknown when the right-most x-forwarded-for token is invalid", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("returns unknown when the left-most x-forwarded-for token is invalid", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": "5.6.7.8, not-an-ip" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": "not-an-ip, 5.6.7.8" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("unknown"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("prefers x-real-ip over x-forwarded-for when both are present", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-real-ip": "1.2.3.4", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": "5.6.7.8, 9.10.11.12" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("1.2.3.4"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+169
to
+177
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. 167行目の Prompt To Fix With AIThis is a comment left during a code review.
Path: src/lib/__tests__/rateLimit.test.ts
Line: 169-177
Comment:
**テストが `describe` ブロックの外側に配置されている**
167行目の `});` で `describe("getClientIp", ...)` ブロックが閉じられていますが、新しく追加された `it("prefers x-real-ip over x-forwarded-for when both are present", ...)` はそのブロックの外側(169行目)に孤立しています。このテストはテストランナーによって正しく処理されず、無視されるか実行時エラーになる可能性があります。実際の動作検証が行われないまま、実装の正しさが確認されていない状態になります。
How can I resolve this? If you propose a fix, please make it concise.
Comment on lines
+159
to
+177
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/lib/__tests__/rateLimit.test.ts
Line: 159-177
Comment:
最後の `it(...)` ブロックが `describe("getClientIp", ...)` の外に出てしまっています。正しく内側に移動する必要があります。
```suggestion
it("returns unknown when the left-most x-forwarded-for token is invalid", () => {
const req = new Request("http://localhost", {
headers: {
"x-forwarded-for": "not-an-ip, 5.6.7.8"
}
});
expect(getClientIp(req)).toBe("unknown");
});
it("prefers x-real-ip over x-forwarded-for when both are present", () => {
const req = new Request("http://localhost", {
headers: {
"x-real-ip": "1.2.3.4",
"x-forwarded-for": "5.6.7.8, 9.10.11.12"
}
});
expect(getClientIp(req)).toBe("1.2.3.4");
});
});
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("ignores x-real-ip if it is an invalid IP and falls back to x-forwarded-for", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-real-ip": "invalid-ip", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-forwarded-for": "5.6.7.8, 9.10.11.12" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("5.6.7.8"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it("ignores x-real-ip if it is an invalid IP and returns unknown if x-forwarded-for is missing", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const req = new Request("http://localhost", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "x-real-ip": "invalid-ip" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(getClientIp(req)).toBe("unknown"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,10 +67,16 @@ function isValidIp(value: string): boolean { | |
| } | ||
|
|
||
| export function getClientIp(request: Request): string { | ||
| const realIp = request.headers.get("x-real-ip"); | ||
| if (realIp) { | ||
| const trimmedRealIp = realIp.trim(); | ||
| if (isValidIp(trimmedRealIp)) return trimmedRealIp; | ||
| } | ||
|
Comment on lines
+70
to
+74
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/lib/rateLimit.ts
Line: 70-74
Comment:
**`x-real-ip` は攻撃者がスプーフィングできる可能性がある**
`x-real-ip` は通常のHTTPヘッダーにすぎず、リバースプロキシがクライアントからのこのヘッダーを除去・上書きしない設定になっている場合、攻撃者は `X-Real-IP: 127.0.0.1` のような任意のIPを送信してレートリミットを回避できます。変更前のコードは意図的に `x-real-ip` を信頼しない設計(テスト名も `"does not trust x-real-ip"` )でしたが、本PRでは検証なしに最優先のソースとして扱うよう変更されています。プロキシ側で確実にこのヘッダーが上書きされる保証がない限り、これはセキュリティ上のリグレッションになる可能性があります。
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| const forwardedFor = request.headers.get("x-forwarded-for"); | ||
| if (!forwardedFor) return "unknown"; | ||
|
|
||
| const proxyObservedIp = forwardedFor.split(",").at(-1)?.trim(); | ||
| const proxyObservedIp = forwardedFor.split(",")[0]?.trim(); | ||
| if (proxyObservedIp && isValidIp(proxyObservedIp)) return proxyObservedIp; | ||
|
|
||
| return "unknown"; | ||
|
|
||
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.
The
describeblock forgetClientIpis closed prematurely on line 167, which leaves the new test caseprefers x-real-ip over x-forwarded-for when both are presentoutside of thedescribeblock. This breaks the logical grouping of the tests and leaves the test at the root level of the file. Please move the closing brace});to the end of the file to properly include this test case within thegetClientIpsuite. Additionally, ensure the test function has an explicit return type to maintain type safety.References