Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/api/paste/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
calculateExpiration,
calculateTTL,
validatePasteSize,
getClientIp,
} from '@/lib/validation';
import { getClientIp } from '@/lib/ip';

// Rate limiting: 10 pastes per hour per IP
const RATE_LIMIT_MAX = 10;
Expand Down
46 changes: 46 additions & 0 deletions src/lib/ip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test } from 'node:test';
import assert from 'node:assert';
import { getClientIp } from './ip.ts';

test('getClientIp prioritizes x-real-ip', () => {
const headers = new Headers();
headers.set('x-real-ip', '1.2.3.4');
headers.set('x-forwarded-for', '5.6.7.8');
const ip = getClientIp(headers);
assert.strictEqual(ip, '1.2.3.4');
});

test('getClientIp takes the LAST IP in x-forwarded-for to prevent spoofing', () => {
const headers = new Headers();
// 1.1.1.1 is spoofed by client, 2.2.2.2 is real client IP added by proxy
headers.set('x-forwarded-for', '1.1.1.1, 2.2.2.2');
const ip = getClientIp(headers);
assert.strictEqual(ip, '2.2.2.2');
});

test('getClientIp handles multiple IPs in x-forwarded-for', () => {
const headers = new Headers();
headers.set('x-forwarded-for', '1.1.1.1, 2.2.2.2, 3.3.3.3');
const ip = getClientIp(headers);
assert.strictEqual(ip, '3.3.3.3');
});

test('getClientIp handles x-forwarded-for with whitespace', () => {
const headers = new Headers();
headers.set('x-forwarded-for', ' 1.1.1.1 , 2.2.2.2 ');
const ip = getClientIp(headers);
assert.strictEqual(ip, '2.2.2.2');
});

test('getClientIp returns unknown if no headers are present', () => {
const headers = new Headers();
const ip = getClientIp(headers);
assert.strictEqual(ip, 'unknown');
});

test('getClientIp handles empty x-forwarded-for', () => {
const headers = new Headers();
headers.set('x-forwarded-for', '');
const ip = getClientIp(headers);
assert.strictEqual(ip, 'unknown');
});
28 changes: 28 additions & 0 deletions src/lib/ip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Utility to securely retrieve client IP from request headers
*/

/**
* Get client IP from request headers
* Handles proxy headers securely by prioritizing x-real-ip and
* taking the last entry in x-forwarded-for to prevent spoofing.
*/
export function getClientIp(headers: Headers): string {
// 1. Check x-real-ip (often set by Vercel, Nginx, etc.)
const xRealIp = headers.get('x-real-ip');
if (xRealIp) return xRealIp;

// 2. Check x-forwarded-for
const xForwardedFor = headers.get('x-forwarded-for');
if (xForwardedFor) {
// We take the LAST IP in the list.
// If the client spoofs XFF, their spoofed IP will be at the beginning.
// The real client IP (seen by the proxy) will be appended to the list.
const ips = xForwardedFor.split(',').map(ip => ip.trim()).filter(Boolean);
if (ips.length > 0) {
return ips[ips.length - 1];
}
}

return 'unknown';
}
10 changes: 0 additions & 10 deletions src/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,3 @@ export function validatePasteSize(ciphertext: string): boolean {
return estimatedSize <= MAX_PASTE_SIZE;
}

/**
* Get client IP from request headers
*/
export function getClientIp(headers: Headers): string {
return (
headers.get('x-forwarded-for')?.split(',')[0] ||
headers.get('x-real-ip') ||
'unknown'
);
}