Security hardening and codebase cleanup

Security fixes:
- Replace Caesar cipher with electron.safeStorage for account credentials
- Validate shell.openExternal URLs (allow only http/https protocols)
- Remove rejectUnauthorized:false from all HTTPS calls
- Add redirect domain validation to auto-updater
- Fix XSS in matchmaker popup (innerHTML → textContent/createTextNode)
- Add IPC config key whitelist to prevent arbitrary store access
- Credentials never sent to renderer; decrypted on-demand via IPC

Optimizations and cleanup:
- Simplify onBeforeRequest from double-registration to single handler
- Lazy-init matchmaker popup DOM (defer until first use)
- Invalidate game config cache immediately on write, not on flush
- Remove unused STANDARD_ASSET_RE and KeybindDef exports
- Deduplicate Keybind type (import from config.ts)
- Replace custom hasOwn wrapper with Object.hasOwn

Bug fix:
- Stop Krunker's global keydown handler from eating keystrokes in
  alt manager input fields (stopPropagation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 08:54:52 -08:00
parent 96e0cbfc07
commit 819caea65a
7 changed files with 218 additions and 108 deletions
+44 -11
View File
@@ -16,12 +16,25 @@ const UPDATE_CONFIG = {
// Gitea provider (swap these for kpdclient.com migration)
checkUrl: 'https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
assetPattern: /Setup\.exe$/i,
rejectUnauthorized: false,
// Allowed hosts for update check and download (including redirects)
allowedHosts: ['gitea.crjlab.net'],
};
const CHECK_TIMEOUT_MS = 10000;
const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes
/**
* Validate that a redirect URL stays on an allowed host.
*/
function isAllowedRedirect(url: string): boolean {
try {
const parsed = new URL(url);
return UPDATE_CONFIG.allowedHosts.some(h => parsed.hostname === h || parsed.hostname.endsWith('.' + h));
} catch {
return false;
}
}
/**
* Simple semver comparison: returns true if a < b.
* Handles versions like "0.1.0", "1.2.3".
@@ -45,15 +58,19 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
electronLog.log('[KCC-Update] Current version:', currentVersion);
const req = httpsGet(UPDATE_CONFIG.checkUrl, {
rejectUnauthorized: UPDATE_CONFIG.rejectUnauthorized,
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
}, (res) => {
electronLog.log('[KCC-Update] Check response status:', res.statusCode);
// Follow redirects
// Follow redirects (with domain validation)
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
electronLog.log('[KCC-Update] Redirected to:', res.headers.location);
httpsGet(res.headers.location, {
rejectUnauthorized: UPDATE_CONFIG.rejectUnauthorized,
const redirectUrl = res.headers.location;
electronLog.log('[KCC-Update] Redirected to:', redirectUrl);
if (!isAllowedRedirect(redirectUrl)) {
electronLog.error('[KCC-Update] Redirect to untrusted host blocked:', redirectUrl);
resolve(null);
return;
}
httpsGet(redirectUrl, {
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
}, (redirectRes) => {
electronLog.log('[KCC-Update] Redirect response status:', redirectRes.statusCode);
@@ -97,6 +114,13 @@ export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | nul
return;
}
// Validate the download URL points to an allowed host
if (!isAllowedRedirect(setupAsset.browser_download_url)) {
electronLog.error('[KCC-Update] Download URL points to untrusted host:', setupAsset.browser_download_url);
resolve(null);
return;
}
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| Download:', setupAsset.browser_download_url, '| Size:', setupAsset.size);
resolve({
version: remoteVersion,
@@ -131,16 +155,25 @@ export function downloadUpdate(url: string, destPath: string, onProgress: Progre
return new Promise((resolve, reject) => {
const tmpPath = destPath + '.tmp';
function doDownload(downloadUrl: string): void {
function doDownload(downloadUrl: string, redirectCount = 0): void {
if (redirectCount > 5) {
reject(new Error('Too many redirects'));
return;
}
electronLog.log('[KCC-Update] Downloading from:', downloadUrl);
const req = httpsGet(downloadUrl, {
rejectUnauthorized: UPDATE_CONFIG.rejectUnauthorized,
headers: { 'User-Agent': 'KrunkerCivilianClient' },
}, (res) => {
// Follow redirects
// Follow redirects (with domain validation)
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
electronLog.log('[KCC-Update] Download redirected to:', res.headers.location);
doDownload(res.headers.location);
const redirectUrl = res.headers.location;
electronLog.log('[KCC-Update] Download redirected to:', redirectUrl);
if (!isAllowedRedirect(redirectUrl)) {
electronLog.error('[KCC-Update] Download redirect to untrusted host blocked:', redirectUrl);
reject(new Error('Download redirect to untrusted host: ' + redirectUrl));
return;
}
doDownload(redirectUrl, redirectCount + 1);
return;
}