Skip to content

Commit 924a1f6

Browse files
authored
fix: sanitize svg colors and secure track-user ip rate limiting (JhaSourav07#1996)
## Description Fixes JhaSourav07#1983 This PR addresses two security issues: 1. **SVG XSS Mitigation**: Explicitly passes user-supplied parameters through `sanitizeHexColor` before injecting them into the fallback error SVG (`generateNotFoundSVG`), mitigating any potential risk of unescaped hex color XSS. 2. **MongoDB Exhaustion/DoS Mitigation**: Hardens the IP resolution logic in the `/api/track-user` endpoint by resolving the true client IP using Vercel's trusted headers (`x-real-ip` and the last IP in the `x-forwarded-for` chain). This effectively patches the IP-spoofing rate-limit bypass. ## Pillar - [ ] dYZ" Pillar 1 ?" New Theme Design - [ ] dY"? Pillar 2 ?" Geometric SVG Improvement - [ ] dY ? Pillar 3 ?" Timezone Logic Optimization - [x] dY>,? Other (Bug fix, refactoring, docs) ## Visual Preview N/A (Backend Security Fixes) ## Checklist before requesting a review: - [x] I have read the `CONTRIBUTING.md` file. - [x] I have tested these changes locally (`localhost:3000/api/streak?user=YOUR_USERNAME`). - [x] I have run `npm run format` and `npm run lint` locally and resolved all errors (CI will fail otherwise). - [x] My commits follow the Conventional Commits format (e.g., `feat(themes): ...`, `fix(calculate): ...`). - [x] I have updated `README.md` if I added a new theme or URL parameter. - [x] I have started the repo. - [x] I have made sure that i have only one commit to merge in this PR. - [x] The SVG output matches the CommitPulse "premium quality" aesthetic standard (no raw elements, smooth animations, correct fonts). - [x] (Recommended) I joined the CommitPulse Discord community for contributor discussions, mentorship, and faster PR support.
2 parents 8bb642a + e5e492a commit 924a1f6

5 files changed

Lines changed: 139 additions & 56 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;";
@@ -299,15 +300,15 @@ function buildErrorResponse(error: unknown, parseResult: ParseResult): NextRespo
299300
message.toLowerCase().includes('validation') ||
300301
message.toLowerCase().includes('strictly for organizations');
301302

302-
const errBg = `#${(parseResult.success && parseResult.data.bg) || '0d1117'}`;
303-
const errAccent = `#${
303+
const errBg = `#${sanitizeHexColor(parseResult.success ? parseResult.data.bg : undefined, '0d1117')}`;
304+
const errAccentRaw =
304305
(parseResult.success &&
305306
(Array.isArray(parseResult.data.accent)
306307
? parseResult.data.accent[parseResult.data.accent.length - 1]
307308
: parseResult.data.accent)) ||
308-
'58a6ff'
309-
}`;
310-
const errText = `#${(parseResult.success && parseResult.data.text) || 'c9d1d9'}`;
309+
undefined;
310+
const errAccent = `#${sanitizeHexColor(errAccentRaw, '58a6ff')}`;
311+
const errText = `#${sanitizeHexColor(parseResult.success ? parseResult.data.text : undefined, 'c9d1d9')}`;
311312
const errRadius = parseResult.success
312313
? (() => {
313314
const r = Number(parseResult.data.radius);

app/components/navbar.test.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,17 @@ vi.mock('lucide-react', () => ({
5858

5959
describe('Navbar mobile menu', () => {
6060
beforeEach(() => {
61+
Object.defineProperty(window, 'localStorage', {
62+
value: {
63+
getItem: vi.fn(),
64+
setItem: vi.fn(),
65+
clear: vi.fn(),
66+
},
67+
writable: true,
68+
});
6169
window.innerWidth = 500;
6270
mockMatchMedia(false);
63-
window.localStorage.clear();
71+
window.localStorage?.clear();
6472
document.documentElement.className = '';
6573
});
6674

@@ -106,8 +114,16 @@ describe('Navbar mobile menu', () => {
106114

107115
describe('Navbar responsive breakpoints', () => {
108116
beforeEach(() => {
117+
Object.defineProperty(window, 'localStorage', {
118+
value: {
119+
getItem: vi.fn(),
120+
setItem: vi.fn(),
121+
clear: vi.fn(),
122+
},
123+
writable: true,
124+
});
109125
window.innerWidth = 500;
110-
window.localStorage.clear();
126+
window.localStorage?.clear();
111127
document.documentElement.className = '';
112128
});
113129

app/page.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ vi.mock('next/link', () => ({
2929
),
3030
}));
3131

32+
vi.mock('@/utils/tracking', () => ({
33+
trackUser: vi.fn(),
34+
}));
35+
3236
// Mock GSAP so FeatureCards don't break in JSDOM
3337
vi.mock('gsap', () => {
3438
const tween = { kill: vi.fn() };

0 commit comments

Comments
 (0)