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:
+44
-11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user