Description
The registration form's Full Name field accepts arbitrary free text with no sanitization or length limit. That value is persisted into users.json/overall.json/changes.json and later inserted into the DOM via raw innerHTML string interpolation in three places, instead of textContent (which is used correctly elsewhere, e.g. render.js).
Steps to Reproduce
- On
/registration, submit a valid LeetCode username with Full Name set to <img src=x onerror=alert(document.cookie)>.
- Once the entry syncs into the leaderboard data, visit
/leaderboard.
- Enable "compare" mode and select that user, or open the "recent changes" panel after that user shows up in a rank-change/new-user entry.
- The payload executes.
Expected Behavior
Any user-supplied name should be rendered as plain text, with no HTML/script execution, regardless of content.
Actual Behavior
The name is interpolated unescaped into innerHTML and executes as HTML/JS.
Root Cause
frontend/registration.html restricts the LeetCode username to [A-Za-z0-9_-] but applies no equivalent restriction to name:
body: JSON.stringify({ name: name.trim(), id: leetcodeId.trim() }),
That unsanitized value then hits three unsafe sinks:
frontend/js/leaderboard/compare.js (~line 237):
const namesStr = window.selectedUsers.map((u) => u.name).join(", ");
bar.innerHTML = `... ${namesStr} ...`;
frontend/js/leaderboard/compare.js (~lines 431, 501-508):
const headers = ["Metric", ...window.selectedUsers.map((u) => u.name)];
headers.forEach((h) => { html += `<th>${h}</th>`; });
frontend/js/leaderboard/sync-changes.js (~line 55):
html += `<strong style="color:#fff;">${rc.username}</strong> moved #${rc.old_rank} → ...`;
Suggested Fix
Replace these three innerHTML sites with safe DOM construction (textContent / document.createTextNode), matching the pattern already used in render.js. As defense in depth, also consider a length cap + character allowlist on name at registration time.
Affected Files
frontend/js/leaderboard/compare.js
frontend/js/leaderboard/sync-changes.js
frontend/registration.html
Description
The registration form's Full Name field accepts arbitrary free text with no sanitization or length limit. That value is persisted into
users.json/overall.json/changes.jsonand later inserted into the DOM via rawinnerHTMLstring interpolation in three places, instead oftextContent(which is used correctly elsewhere, e.g.render.js).Steps to Reproduce
/registration, submit a valid LeetCode username with Full Name set to<img src=x onerror=alert(document.cookie)>./leaderboard.Expected Behavior
Any user-supplied name should be rendered as plain text, with no HTML/script execution, regardless of content.
Actual Behavior
The name is interpolated unescaped into
innerHTMLand executes as HTML/JS.Root Cause
frontend/registration.htmlrestricts the LeetCode username to[A-Za-z0-9_-]but applies no equivalent restriction toname:That unsanitized value then hits three unsafe sinks:
frontend/js/leaderboard/compare.js(~line 237):frontend/js/leaderboard/compare.js(~lines 431, 501-508):frontend/js/leaderboard/sync-changes.js(~line 55):Suggested Fix
Replace these three
innerHTMLsites with safe DOM construction (textContent/document.createTextNode), matching the pattern already used inrender.js. As defense in depth, also consider a length cap + character allowlist onnameat registration time.Affected Files
frontend/js/leaderboard/compare.jsfrontend/js/leaderboard/sync-changes.jsfrontend/registration.html