-
Notifications
You must be signed in to change notification settings - Fork 19
fix(security): restore per-IP rate-limit key when no proxy header is set #23
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 |
|---|---|---|
|
|
@@ -54,7 +54,7 @@ describe("rate-limit utility", () => { | |
| expect(Number(headers["retry-after"])).toBeGreaterThan(0); | ||
| }); | ||
|
|
||
| it("extracts client key from request headers", () => { | ||
| it("uses the rightmost entry of the configured proxy header", () => { | ||
| vi.stubEnv("TRUSTED_CLIENT_IP_HEADER", "x-forwarded-for"); | ||
| const request = new Request("https://example.com/api/test", { | ||
| headers: { | ||
|
|
@@ -63,13 +63,38 @@ describe("rate-limit utility", () => { | |
| }, | ||
| }); | ||
|
|
||
| // Rightmost is the proxy-appended peer; leftmost entries are client-forged. | ||
| const key = getRequestClientKey(request); | ||
| expect(key).toBe("ip:203.0.113.11"); | ||
| }); | ||
|
|
||
| it("does not trust client IP headers until a proxy header is configured", () => { | ||
| it("falls back to the rightmost X-Forwarded-For entry when no header is configured", () => { | ||
| // Default deployment (TRUSTED_CLIENT_IP_HEADER unset) must still key per-IP, | ||
| // not collapse every visitor into one global bucket. | ||
| const request = new Request("https://example.com/api/test", { | ||
| headers: { "x-forwarded-for": "203.0.113.10" }, | ||
| headers: { "x-forwarded-for": "198.51.100.7, 203.0.113.10" }, | ||
| }); | ||
|
|
||
| expect(getRequestClientKey(request)).toBe("ip:203.0.113.10"); | ||
| }); | ||
|
|
||
| it("does not let a client forge a fresh key by prepending X-Forwarded-For entries", () => { | ||
| const attacker = new Request("https://example.com/api/test", { | ||
| headers: { "x-forwarded-for": "10.0.0.1, 203.0.113.10" }, | ||
| }); | ||
| const attackerAgain = new Request("https://example.com/api/test", { | ||
| headers: { "x-forwarded-for": "10.0.0.2, 203.0.113.10" }, | ||
| }); | ||
|
|
||
| // The forgeable leftmost entry changes, but the proxy-appended rightmost | ||
| // one is identical, so both land in the same bucket. | ||
| expect(getRequestClientKey(attacker)).toBe("ip:203.0.113.10"); | ||
| expect(getRequestClientKey(attackerAgain)).toBe("ip:203.0.113.10"); | ||
| }); | ||
|
Comment on lines
+71
to
+93
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
fd -t f | rg '(^|/)(rate-limit\.test|rate-limit|.*rate.*limit.*)\.(ts|tsx|js|jsx)$'
printf '%s\n' '--- references ---'
rg -n -C 4 'TRUSTED_CLIENT_IP_HEADER|getRequestClientKey|stubEnv|unstubAllEnvs|resetAllMocks|afterEach' . -g '*.{ts,tsx,js,jsx}' -g '*.{json,yml,yaml}'Repository: AnxForever/stylekit Length of output: 50376 🏁 Script executed: #!/bin/bash
set -e
for file in tests/unit/lib/rate-limit.test.ts lib/security/rate-limit.ts; do
printf '\n--- %s ---\n' "$file"
wc -l "$file"
cat -n "$file"
done
printf '\n--- test configuration and environment setup ---\n'
fd -t f | rg '(^|/)(vitest|vite|package\.json|.*setup.*)\.(ts|js|json|mjs|cjs)$' | head -80
rg -n -C 3 'unstubEnvs|stubEnv|TRUSTED_CLIENT_IP_HEADER|setupFiles|clearMocks|restoreMocks|mockReset' \
tests vitest.config.* vite.config.* package.json 2>/dev/null | head -240Repository: AnxForever/stylekit Length of output: 14342 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- Vitest configuration ---'
fd -t f | rg '(^|/)(vitest\.config|vite\.config|package\.json|vitest\.setup)\.'
for file in $(fd -t f | rg '(^|/)(vitest\.config|vite\.config|package\.json|vitest\.setup)\.' | head -20); do
printf '\n--- %s ---\n' "$file"
rg -n -C 4 'setupFiles|unstubEnvs|restoreMocks|clearMocks|TRUSTED_CLIENT_IP_HEADER|vitest' "$file" || true
done
printf '\n--- standalone environment-state check ---\n'
node - <<'JS'
const original = process.env.TRUSTED_CLIENT_IP_HEADER;
process.env.TRUSTED_CLIENT_IP_HEADER = "proxy-client-ip";
const configured = process.env.TRUSTED_CLIENT_IP_HEADER?.trim();
const configuredPath = configured || "x-forwarded-for";
process.env.TRUSTED_CLIENT_IP_HEADER = "";
const emptyPath = process.env.TRUSTED_CLIENT_IP_HEADER?.trim() || "x-forwarded-for";
if (configuredPath !== "proxy-client-ip" || emptyPath !== "x-forwarded-for") {
process.exit(1);
}
if (original === undefined) delete process.env.TRUSTED_CLIENT_IP_HEADER;
else process.env.TRUSTED_CLIENT_IP_HEADER = original;
console.log(JSON.stringify({ configuredPath, emptyPath, restored: process.env.TRUSTED_CLIENT_IP_HEADER === original }));
JSRepository: AnxForever/stylekit Length of output: 640 Isolate the default header configuration in both tests. If 🤖 Prompt for AI Agents |
||
|
|
||
| it("degrades to a shared key only when no forwarding header exists at all", () => { | ||
| const request = new Request("https://example.com/api/test", { | ||
| headers: { "user-agent": "StyleKit-Test/1.0" }, | ||
| }); | ||
|
|
||
| expect(getRequestClientKey(request)).toBe("ip: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.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: AnxForever/stylekit
Length of output: 7830
🏁 Script executed:
Repository: AnxForever/stylekit
Length of output: 10730
🏁 Script executed:
Repository: AnxForever/stylekit
Length of output: 10870
🏁 Script executed:
Repository: AnxForever/stylekit
Length of output: 9978
🏁 Script executed:
Repository: AnxForever/stylekit
Length of output: 843
🌐 Web query:
Next.js 16 next start default hostname 0.0.0.0 -H --hostname💡 Result:
In Next.js 16 (including the latest version 16.3.0), the default behavior for the next start command is to listen on localhost (127.0.0.1) [1]. While official CLI documentation and help text may sometimes list the default hostname as 0.0.0.0, this primarily reflects the underlying network behavior where the server will bind to all available network interfaces if no specific hostname is provided [1]. In practice, if you need the server to be explicitly accessible to other devices on your network, you should set the hostname explicitly using the --hostname (-H) flag or the HOSTNAME environment variable [2][3][4][5]. To start your application on all network interfaces, you can use: next start --hostname 0.0.0.0 Or by setting the environment variable: HOSTNAME=0.0.0.0 next start For standalone server deployments, you can similarly define the hostname: HOSTNAME=0.0.0.0 node server.js [3][4]
Citations:
🌐 Web query:
site:nextjs.org/docs next start --hostname default 0.0.0.0 Next.js CLI💡 Result:
The command next start does not have an option named --hostname default. The correct command to specify a hostname is -H or --hostname, followed by the desired IP address or hostname [1][2]. By default, Next.js starts on 0.0.0.0, which allows the application to be accessible from other devices on the network [1][2]. If you wish to explicitly set the hostname to 0.0.0.0, you would use the following command: next start -H 0.0.0.0 To specify a different hostname, such as a specific local IP address, replace 0.0.0.0 with your desired value: next start -H 192.168.1.2 The --hostname flag is used to override the default behavior, not to set it to a keyword like "default" [1][3]. Using an invalid argument like --hostname default will likely result in an error or be ignored by the CLI [1][2].
Citations:
Bind the Next.js process to loopback or restrict port 13000.
ecosystem.config.cjsstartsnext start -p 13000without-H 127.0.0.1. If port 13000 is reachable externally, clients can sendX-Forwarded-Fordirectly and bypass per-IP throttling. Bind Next.js to127.0.0.1or enforce equivalent firewall rules so Nginx is the only public ingress.🤖 Prompt for AI Agents