Skip to content

Commit d47f1ca

Browse files
fix: sanitize svg colors and secure track-user ip rate limiting
1 parent 6a3d87e commit d47f1ca

2 files changed

Lines changed: 12 additions & 7 deletions

File tree

app/api/streak/route.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { getSecondsUntilUTCMidnight, getSecondsUntilMidnightInTimezone } from '@
1616
import type { BadgeParams } from '@/types';
1717
import { themes } from '@/lib/svg/themes';
1818
import { streakParamsSchema } from '@/lib/validations';
19+
import { sanitizeHexColor } from '@/lib/svg/sanitizer';
1920

2021
const SVG_CSP_HEADER =
2122
"default-src 'none'; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src https://fonts.gstatic.com;";
@@ -255,15 +256,15 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo
255256
message.toLowerCase().includes('validation') ||
256257
message.toLowerCase().includes('strictly for organizations');
257258

258-
const errBg = `#${(parseResult.success && parseResult.data.bg) || '0d1117'}`;
259-
const errAccent = `#${
259+
const errBg = `#${sanitizeHexColor((parseResult.success && parseResult.data.bg) || '0d1117')}`;
260+
const errAccentRaw =
260261
(parseResult.success &&
261262
(Array.isArray(parseResult.data.accent)
262263
? parseResult.data.accent[parseResult.data.accent.length - 1]
263264
: parseResult.data.accent)) ||
264-
'58a6ff'
265-
}`;
266-
const errText = `#${(parseResult.success && parseResult.data.text) || 'c9d1d9'}`;
265+
'58a6ff';
266+
const errAccent = `#${sanitizeHexColor(errAccentRaw)}`;
267+
const errText = `#${sanitizeHexColor((parseResult.success && parseResult.data.text) || 'c9d1d9')}`;
267268
const errRadius = parseResult.success
268269
? (() => {
269270
const r = Number(parseResult.data.radius);

app/api/track-user/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import { User } from '@/models/User';
44
import { trackUserRateLimiter } from '@/lib/rate-limit';
55

66
export async function POST(req: Request) {
7-
// Get IP for rate limiting
8-
const ip = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip') || 'unknown';
7+
// Get IP for rate limiting.
8+
// x-real-ip is provided by Vercel/Nginx as the true client IP.
9+
// We fall back to the LAST IP in the x-forwarded-for chain, which is appended by the Vercel proxy.
10+
const forwardedFor = req.headers.get('x-forwarded-for');
11+
const fallbackIp = forwardedFor ? forwardedFor.split(',').pop()?.trim() : 'unknown';
12+
const ip = req.headers.get('x-real-ip') || fallbackIp || 'unknown';
913

1014
if (ip !== 'unknown' && !(await trackUserRateLimiter.check(ip))) {
1115
return NextResponse.json(

0 commit comments

Comments
 (0)