Description
The avatar URL stored via PUT /api/profile has no server-side validation and is rendered client-side using innerHTML without escaping, enabling stored cross-site scripting (XSS).
Vulnerable Code
Server � server.js:713 � avatar stored as-is with no sanitization:
js avatar: req.body.avatar ?? existing?.avatar ?? '',
Client � public/js/app.js:269 � avatar rendered via innerHTML with no escaping:
js avatarEl.innerHTML = <img src="" alt="" />;
The escapeHtml() function exists in the same file (line 388) but is never used for the avatar field.
Exploit Scenario
An attacker sends a direct PUT /api/profile request with a crafted avatar payload containing an onerror event handler. The stored payload is then served to every visitor of any public profile page. Since src="" is invalid, the onerror handler executes immediately, running arbitrary JS in every visitor's browser.
Impact
- Stored XSS � payload persists in DB, fires on every page load
- No auth required to view public profiles � reaches all visitors
- Cookie theft, session hijacking, phishing, defacement
Suggested Fix
Server-side (server.js in PUT /api/profile handler):
js const avatar = req.body.avatar ?? existing?.avatar ?? ''; if (avatar && !/^https?:\/\/.+/i.test(avatar)) { return res.status(400).json({ error: 'Avatar must be a valid URL starting with http:// or https://' }); }
Client-side (public/js/app.js:269): Replace innerHTML with safe DOM API:
js const img = document.createElement('img'); img.src = profile.avatar; img.alt = profile.name || ''; avatarEl.innerHTML = ''; avatarEl.appendChild(img);
File References
- server.js line 713
- public/js/app.js line 269
Description
The avatar URL stored via PUT /api/profile has no server-side validation and is rendered client-side using innerHTML without escaping, enabling stored cross-site scripting (XSS).
Vulnerable Code
Server � server.js:713 � avatar stored as-is with no sanitization:
js avatar: req.body.avatar ?? existing?.avatar ?? '',Client � public/js/app.js:269 � avatar rendered via innerHTML with no escaping:
js avatarEl.innerHTML = <img src="" alt="" />;The escapeHtml() function exists in the same file (line 388) but is never used for the avatar field.
Exploit Scenario
An attacker sends a direct PUT /api/profile request with a crafted avatar payload containing an onerror event handler. The stored payload is then served to every visitor of any public profile page. Since src="" is invalid, the onerror handler executes immediately, running arbitrary JS in every visitor's browser.
Impact
Suggested Fix
Server-side (server.js in PUT /api/profile handler):
js const avatar = req.body.avatar ?? existing?.avatar ?? ''; if (avatar && !/^https?:\/\/.+/i.test(avatar)) { return res.status(400).json({ error: 'Avatar must be a valid URL starting with http:// or https://' }); }Client-side (public/js/app.js:269): Replace innerHTML with safe DOM API:
js const img = document.createElement('img'); img.src = profile.avatar; img.alt = profile.name || ''; avatarEl.innerHTML = ''; avatarEl.appendChild(img);File References