detached 1a6a567

fix(security): use getClientAddress() instead of spoofable headers for IP

commit
1a6a56760d3556c92df7c2781ac13dab5b18034f
author
Kit · · 2026-02-16 11:01 UTC
parent
0ff6d73
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
2 files · +5 -8
M src/routes/api/register/+server.ts +2 -2
@@ -25,7 +25,7 @@ function cleanupOldCaptchaAttempts() {
2525 }
2626 }
2727
28-export const POST: RequestHandler = async ({ request }) => {
28+export const POST: RequestHandler = async ({ request, getClientAddress }) => {
2929 log.debug('New registration attempt received');
3030
3131 // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)
@@ -39,7 +39,7 @@ export const POST: RequestHandler = async ({ request }) => {
3939 }
4040
4141 // Get the IP address for rate limiting and Turnstile validation
42- const ip = request.headers.get('x-forwarded-for') || 'unknown';
42+ const ip = getClientAddress();
4343 const maskedIp = ip
4444 .split('.')
4545 .map((octet, idx) => (idx < 3 ? 'xxx' : octet))
@@ -25,7 +25,7 @@ function cleanupOldCaptchaAttempts() {
25 }25 }
26 }26 }
27 27
28-export const POST: RequestHandler = async ({ request }) => {28+export const POST: RequestHandler = async ({ request, getClientAddress }) => {
29 log.debug('New registration attempt received');29 log.debug('New registration attempt received');
30 30
31 // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)31 // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)
@@ -39,7 +39,7 @@ export const POST: RequestHandler = async ({ request }) => {
39 }39 }
40 40
41 // Get the IP address for rate limiting and Turnstile validation41 // Get the IP address for rate limiting and Turnstile validation
42- const ip = request.headers.get('x-forwarded-for') || 'unknown';42+ const ip = getClientAddress();
43 const maskedIp = ip43 const maskedIp = ip
44 .split('.')44 .split('.')
45 .map((octet, idx) => (idx < 3 ? 'xxx' : octet))45 .map((octet, idx) => (idx < 3 ? 'xxx' : octet))
M src/routes/api/session/login/+server.ts +3 -6
@@ -31,7 +31,7 @@ function cleanupOldAttempts() {
3131 }
3232 }
3333
34-export const POST: RequestHandler = async ({ request, cookies }) => {
34+export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
3535 log.debug('New login attempt received');
3636
3737 // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)
@@ -44,11 +44,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
4444 });
4545 }
4646
47- // Get the IP address for rate limiting and Turnstile validation
48- // Prefer Cloudflare's CF-Connecting-IP header, then x-forwarded-for (first IP), then fallback
49- const cfConnectingIp = request.headers.get('cf-connecting-ip');
50- const forwardedFor = request.headers.get('x-forwarded-for') || '';
51- const ip = cfConnectingIp || forwardedFor.split(',')[0]?.trim() || 'unknown';
47+ // Get the IP address for rate limiting
48+ const ip = getClientAddress();
5249 const maskedIp = ip
5350 .split('.')
5451 .map((octet, idx) => (idx < 3 ? 'xxx' : octet))
@@ -31,7 +31,7 @@ function cleanupOldAttempts() {
31 }31 }
32 }32 }
33 33
34-export const POST: RequestHandler = async ({ request, cookies }) => {34+export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => {
35 log.debug('New login attempt received');35 log.debug('New login attempt received');
36 36
37 // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)37 // Lazy cleanup on each request (replaces setInterval for Cloudflare compatibility)
@@ -44,11 +44,8 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
44 });44 });
45 }45 }
46 46
47- // Get the IP address for rate limiting and Turnstile validation47+ // Get the IP address for rate limiting
48- // Prefer Cloudflare's CF-Connecting-IP header, then x-forwarded-for (first IP), then fallback48+ const ip = getClientAddress();
49- const cfConnectingIp = request.headers.get('cf-connecting-ip');
50- const forwardedFor = request.headers.get('x-forwarded-for') || '';
51- const ip = cfConnectingIp || forwardedFor.split(',')[0]?.trim() || 'unknown';
52 const maskedIp = ip49 const maskedIp = ip
53 .split('.')50 .split('.')
54 .map((octet, idx) => (idx < 3 ? 'xxx' : octet))51 .map((octet, idx) => (idx < 3 ? 'xxx' : octet))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close