fix: escape external data in matchmaker and changelog to prevent XSS
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
// Shows release notes in a Shadow DOM modal when the client version changes.
|
// Shows release notes in a Shadow DOM modal when the client version changes.
|
||||||
|
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
import { escapeHtml } from './utils';
|
||||||
|
|
||||||
function versionLessThan(a: string, b: string): boolean {
|
function versionLessThan(a: string, b: string): boolean {
|
||||||
const pa = a.split('.').map(Number);
|
const pa = a.split('.').map(Number);
|
||||||
@@ -16,14 +17,25 @@ function versionLessThan(a: string, b: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeUrl(url: string): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.protocol === 'https:' || parsed.protocol === 'http:') return escapeHtml(url);
|
||||||
|
} catch { /* invalid URL */ }
|
||||||
|
return '#';
|
||||||
|
}
|
||||||
|
|
||||||
function renderMarkdown(md: string): string {
|
function renderMarkdown(md: string): string {
|
||||||
const html = md
|
// Escape all HTML first, then apply markdown formatting to the safe text
|
||||||
|
const escaped = escapeHtml(md);
|
||||||
|
const html = escaped
|
||||||
.replace(/### (.+)/g, '<h3>$1</h3>')
|
.replace(/### (.+)/g, '<h3>$1</h3>')
|
||||||
.replace(/## (.+)/g, '<h2>$1</h2>')
|
.replace(/## (.+)/g, '<h2>$1</h2>')
|
||||||
.replace(/# (.+)/g, '<h1>$1</h1>')
|
.replace(/# (.+)/g, '<h1>$1</h1>')
|
||||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) =>
|
||||||
|
`<a href="${sanitizeUrl(url)}" target="_blank">${text}</a>`);
|
||||||
|
|
||||||
// Convert list items
|
// Convert list items
|
||||||
const lines = html.split('\n');
|
const lines = html.split('\n');
|
||||||
@@ -94,7 +106,7 @@ function showChangelogPopup(version: string, body: string): void {
|
|||||||
|
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.className = 'header';
|
header.className = 'header';
|
||||||
header.innerHTML = `<h2>What's New in v${version}</h2>`;
|
header.innerHTML = `<h2>What's New in v${escapeHtml(version)}</h2>`;
|
||||||
const closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.className = 'close-btn';
|
closeBtn.className = 'close-btn';
|
||||||
closeBtn.textContent = '\u2715';
|
closeBtn.textContent = '\u2715';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import type { Keybind } from '../main/config';
|
import type { Keybind } from '../main/config';
|
||||||
import type { SavedConsole } from './utils';
|
import { escapeHtml, type SavedConsole } from './utils';
|
||||||
|
|
||||||
// Full array — indices must match the server's gamemode IDs (game[4].g)
|
// Full array — indices must match the server's gamemode IDs (game[4].g)
|
||||||
export const MATCHMAKER_GAMEMODES = ['Free for All', 'Team Deathmatch', 'Hardpoint', 'Capture the Flag', 'Parkour', 'Hide & Seek', 'Infected', 'Race', 'Last Man Standing', 'Simon Says', 'Gun Game', 'Prop Hunt', 'Boss Hunt', 'Classic FFA', 'Deposit', 'Stalker', 'King of the Hill', 'One in the Chamber', 'Trade', 'Kill Confirmed', 'Defuse', 'Sharp Shooter', 'Traitor', 'Raid', 'Blitz', 'Domination', 'Squad Deathmatch', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers', 'Bighead FFA'];
|
export const MATCHMAKER_GAMEMODES = ['Free for All', 'Team Deathmatch', 'Hardpoint', 'Capture the Flag', 'Parkour', 'Hide & Seek', 'Infected', 'Race', 'Last Man Standing', 'Simon Says', 'Gun Game', 'Prop Hunt', 'Boss Hunt', 'Classic FFA', 'Deposit', 'Stalker', 'King of the Hill', 'One in the Chamber', 'Trade', 'Kill Confirmed', 'Defuse', 'Sharp Shooter', 'Traitor', 'Raid', 'Blitz', 'Domination', 'Squad Deathmatch', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers', 'Bighead FFA'];
|
||||||
@@ -242,7 +242,7 @@ function showResultPopup(game: MatchmakerGame): void {
|
|||||||
} else {
|
} else {
|
||||||
popupTitle.innerText = 'Game Found!';
|
popupTitle.innerText = 'Game Found!';
|
||||||
const regionName = MATCHMAKER_REGION_NAMES[game.region] ?? 'Unknown Region';
|
const regionName = MATCHMAKER_REGION_NAMES[game.region] ?? 'Unknown Region';
|
||||||
popupDescription.innerHTML = `${game.gamemode} on ${game.map} (${regionName})<br/>${game.playerCount}/${game.playerLimit} Players, ${secondsToTimestring(game.remainingTime)} Left`;
|
popupDescription.innerHTML = `${escapeHtml(game.gamemode)} on ${escapeHtml(game.map)} (${escapeHtml(regionName)})<br/>${game.playerCount}/${game.playerLimit} Players, ${secondsToTimestring(game.remainingTime)} Left`;
|
||||||
popupConfirmBtn.style.display = 'block';
|
popupConfirmBtn.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,8 +438,8 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
|||||||
found.className = 'mm-feed-entry mm-pass';
|
found.className = 'mm-feed-entry mm-pass';
|
||||||
found.style.cssText = 'font-size:1.1em;justify-content:center;';
|
found.style.cssText = 'font-size:1.1em;justify-content:center;';
|
||||||
found.innerHTML =
|
found.innerHTML =
|
||||||
`<span class="mm-feed-region">${best.region}</span>` +
|
`<span class="mm-feed-region">${escapeHtml(best.region)}</span>` +
|
||||||
`<span class="mm-feed-map">${best.map}</span>` +
|
`<span class="mm-feed-map">${escapeHtml(best.map)}</span>` +
|
||||||
`<span class="mm-feed-players">${best.playerCount}/${best.playerLimit}</span>`;
|
`<span class="mm-feed-players">${best.playerCount}/${best.playerLimit}</span>`;
|
||||||
searchFeed.appendChild(found);
|
searchFeed.appendChild(found);
|
||||||
searchCounter.textContent = `${best.gamemode} \u00B7 ${regionName} \u00B7 ${pings[best.region] ?? '?'}ms`;
|
searchCounter.textContent = `${best.gamemode} \u00B7 ${regionName} \u00B7 ${pings[best.region] ?? '?'}ms`;
|
||||||
|
|||||||
Reference in New Issue
Block a user