From 6cc9b0934f193d0242a3b41ccf7cbfa3ba626489 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 7 Jun 2026 11:01:02 +0000 Subject: [PATCH] feat: add CSP, centralized logger, replace empty catches with proper logging - Add Content-Security-Policy via onHeadersReceived (production only) Blocks eval, external scripts, object/embed; allows HTTPS/localhost for API calls - Add centralized logger utility (main/utils/logger.ts) - Replace empty catch blocks in main.ts, mainWindow.ts, codexAppServer.ts, LocalSettingsService.ts with structured logger calls - Remove backward compatibility in SecureStorageService: unencrypted values are now discarded instead of being passed through as plaintext --- packages/app/src/main/codexAppServer.ts | 28 ++++++++++++----- packages/app/src/main/main.ts | 24 +++++++++----- .../main/security/contentSecurityPolicy.ts | 30 ++++++++++++++++++ .../src/main/services/LocalSettingsService.ts | 4 ++- .../src/main/services/SecureStorageService.ts | 19 ++++++++---- packages/app/src/main/utils/logger.ts | 31 +++++++++++++++++++ packages/app/src/main/windows/mainWindow.ts | 13 ++++++-- 7 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 packages/app/src/main/security/contentSecurityPolicy.ts create mode 100644 packages/app/src/main/utils/logger.ts diff --git a/packages/app/src/main/codexAppServer.ts b/packages/app/src/main/codexAppServer.ts index bcfc9d3..8935882 100644 --- a/packages/app/src/main/codexAppServer.ts +++ b/packages/app/src/main/codexAppServer.ts @@ -5,6 +5,7 @@ import { existsSync, statSync } from "node:fs"; import { dirname, join } from "node:path"; import { app } from "electron"; import { discoverExistingCodexPaths } from "./codexNativeDiscovery"; +import { logger } from "./utils/logger"; import type { CodexIncomingMessage, CodexNotifyParams, @@ -164,16 +165,24 @@ export class CodexAppServer { this.rejectPending(new Error("codex app-server stopped")); try { this.rl?.close(); - } catch {} + } catch (error) { + logger.warn("codex-server", "failed to close readline", error); + } try { proc.stdin?.destroy(); - } catch {} + } catch (error) { + logger.warn("codex-server", "failed to destroy stdin", error); + } try { proc.stdout?.destroy(); - } catch {} + } catch (error) { + logger.warn("codex-server", "failed to destroy stdout", error); + } try { proc.stderr?.destroy(); - } catch {} + } catch (error) { + logger.warn("codex-server", "failed to destroy stderr", error); + } try { if (process.platform === "win32" && pid) { const killer = spawn("taskkill.exe", ["/pid", String(pid), "/t", "/f"], { @@ -185,14 +194,19 @@ export class CodexAppServer { } else { proc.kill(); } - } catch { + } catch (error) { + logger.warn("codex-server", "primary kill failed, retrying", error); try { proc.kill(); - } catch {} + } catch (retryError) { + logger.warn("codex-server", "retry kill also failed", retryError); + } } try { proc.unref(); - } catch {} + } catch (error) { + logger.warn("codex-server", "failed to unref process", error); + } this.proc = undefined; this.rl = undefined; } diff --git a/packages/app/src/main/main.ts b/packages/app/src/main/main.ts index 34316a5..c01adb5 100644 --- a/packages/app/src/main/main.ts +++ b/packages/app/src/main/main.ts @@ -2,6 +2,8 @@ import { app, BrowserWindow, Menu } from "electron"; import { readFile, rm, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { join } from "node:path"; +import { installContentSecurityPolicy } from "./security/contentSecurityPolicy"; +import { logger } from "./utils/logger"; import { IPC_APP_CHANNELS, IPC_EVENT_CHANNELS, @@ -66,7 +68,9 @@ function sendToRenderer(channel: string, payload: unknown) { if (!mainWindow || mainWindow.isDestroyed()) return; try { mainWindow.webContents.send(channel, payload); - } catch {} + } catch (error) { + logger.warn("ipc", `sendToRenderer failed on channel '${channel}'`, error); + } } const updateService = new UpdateService((payload) => { @@ -107,12 +111,12 @@ function stopCodexServersForClose(_reason: string) { try { codexServerManager.stopAll(); } catch (error) { - console.warn("[app-close] stop codex servers failed", error); + logger.warn("app-close", "stop codex servers failed", error); } try { deepSeekResponsesProxyService.stop(); } catch (error) { - console.warn("[app-close] stop DeepSeek proxy failed", error); + logger.warn("app-close", "stop DeepSeek proxy failed", error); } } @@ -125,7 +129,7 @@ function clearAppCloseForceExitWatchdog() { function armAppCloseForceExitWatchdog() { clearAppCloseForceExitWatchdog(); appCloseForceExitTimer = setTimeout(() => { - console.warn("[app-close] force exiting after close watchdog timeout"); + logger.warn("app-close", "force exiting after close watchdog timeout"); stopCodexServersForClose("force-exit-watchdog"); app.exit(0); }, APP_CLOSE_FORCE_EXIT_MS); @@ -158,7 +162,7 @@ async function runAppCloseFlow(win: BrowserWindow): Promise { if (!win.isDestroyed()) win.close(); })() .catch((error) => { - console.error("[app-close] flow failed", error); + logger.error("app-close", "flow failed", error); allowMainWindowClose = true; if (!win.isDestroyed()) win.close(); }) @@ -189,6 +193,10 @@ app Menu.setApplicationMenu(null); } + if (!isDev) { + installContentSecurityPolicy(); + } + const historyCachePath = join(app.getPath("userData"), "thread-history-cache.json"); const historyStore = new HistoryStore(historyCachePath); const historyService = new HistoryService(historyStore); @@ -231,7 +239,9 @@ app const raw = await readFile(historyCachePath, "utf8"); const parsed = JSON.parse(raw); items = Array.isArray(parsed?.items) ? parsed.items.length : 0; - } catch {} + } catch (error) { + logger.warn("cache", "failed to read history cache stats", error); + } return { items, bytes, @@ -317,6 +327,6 @@ app }); }) .catch((error) => { - console.error("[main] app bootstrap failed", error); + logger.error("main", "app bootstrap failed", error); app.exit(1); }); diff --git a/packages/app/src/main/security/contentSecurityPolicy.ts b/packages/app/src/main/security/contentSecurityPolicy.ts new file mode 100644 index 0000000..bc4d53e --- /dev/null +++ b/packages/app/src/main/security/contentSecurityPolicy.ts @@ -0,0 +1,30 @@ +import { session } from "electron"; + +/** + * 生产环境 CSP 策略: + * - 禁止 eval 和外部脚本注入 + * - 允许内联样式(Vue/Tailwind 需要)和内联脚本(index.html 主题初始化) + * - 图片允许 data: URI 和 HTTPS 源 + * - 网络请求限制为 HTTPS 和本地开发地址 + */ +const CSP_DIRECTIVES = [ + "default-src 'self'", + "script-src 'self' 'unsafe-inline'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "connect-src 'self' https: http://localhost:* ws://localhost:*", + "font-src 'self' data:", + "object-src 'none'", + "base-uri 'self'", +].join("; "); + +export function installContentSecurityPolicy(): void { + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + callback({ + responseHeaders: { + ...details.responseHeaders, + "Content-Security-Policy": [CSP_DIRECTIVES], + }, + }); + }); +} diff --git a/packages/app/src/main/services/LocalSettingsService.ts b/packages/app/src/main/services/LocalSettingsService.ts index a29b2d3..5b10e9f 100644 --- a/packages/app/src/main/services/LocalSettingsService.ts +++ b/packages/app/src/main/services/LocalSettingsService.ts @@ -8,6 +8,7 @@ import { type UserLocalSettingsPatch, } from "../../common/localSettings"; import { SecureStorageService } from "./SecureStorageService"; +import { logger } from "../utils/logger"; function tryParseJson(text: string): unknown { try { @@ -62,7 +63,8 @@ export class LocalSettingsService { const raw = await readFile(this.filePath, "utf8"); const settings = normalizeUserLocalSettings(tryParseJson(raw)); return { exists: true, settings: this.decryptApiKeys(settings) }; - } catch { + } catch (error) { + logger.info("settings", `settings file not available, using defaults (${this.filePath})`); return { exists: false, settings: normalizeUserLocalSettings(DEFAULT_USER_LOCAL_SETTINGS) }; } } diff --git a/packages/app/src/main/services/SecureStorageService.ts b/packages/app/src/main/services/SecureStorageService.ts index 2be015c..90ba78b 100644 --- a/packages/app/src/main/services/SecureStorageService.ts +++ b/packages/app/src/main/services/SecureStorageService.ts @@ -1,4 +1,5 @@ import { safeStorage } from "electron"; +import { logger } from "../utils/logger"; const ENCRYPTED_PREFIX = "enc:"; @@ -6,8 +7,8 @@ const ENCRYPTED_PREFIX = "enc:"; * Wraps Electron's safeStorage (DPAPI on Windows) to encrypt/decrypt * sensitive strings before persisting them to disk. * - * Encrypted values are stored as "enc:" so the reader can - * distinguish them from legacy plaintext values and migrate gracefully. + * Encrypted values are stored as "enc:". + * Unrecognized values (no prefix) are treated as invalid and discarded. */ export class SecureStorageService { isAvailable(): boolean { @@ -16,7 +17,10 @@ export class SecureStorageService { encrypt(plaintext: string | null): string | null { if (plaintext == null || plaintext === "") return plaintext; - if (!this.isAvailable()) return plaintext; + if (!this.isAvailable()) { + logger.warn("secure-storage", "encryption unavailable, value will not be persisted securely"); + return null; + } const encrypted = safeStorage.encryptString(plaintext); return `${ENCRYPTED_PREFIX}${encrypted.toString("base64")}`; } @@ -24,10 +28,13 @@ export class SecureStorageService { decrypt(stored: string | null): string | null { if (stored == null || stored === "") return stored; if (!stored.startsWith(ENCRYPTED_PREFIX)) { - // Legacy plaintext value — return as-is for backward compatibility. - return stored; + logger.warn("secure-storage", "discarding unencrypted value — re-enter the key in settings"); + return null; + } + if (!this.isAvailable()) { + logger.warn("secure-storage", "decryption unavailable, cannot read encrypted value"); + return null; } - if (!this.isAvailable()) return stored; const base64 = stored.slice(ENCRYPTED_PREFIX.length); const buffer = Buffer.from(base64, "base64"); return safeStorage.decryptString(buffer); diff --git a/packages/app/src/main/utils/logger.ts b/packages/app/src/main/utils/logger.ts new file mode 100644 index 0000000..685e4d2 --- /dev/null +++ b/packages/app/src/main/utils/logger.ts @@ -0,0 +1,31 @@ +/** + * 主进程统一日志工具。 + * + * 替代散落的 console.warn/console.error 和空 catch,为后续接入文件日志或上报预留统一入口。 + */ + +type LogLevel = "debug" | "info" | "warn" | "error"; + +function formatTag(tag: string): string { + return `[${tag}]`; +} + +function log(level: LogLevel, tag: string, message: string, error?: unknown): void { + const prefix = formatTag(tag); + if (level === "error") { + console.error(prefix, message, error ?? ""); + } else if (level === "warn") { + console.warn(prefix, message, error ?? ""); + } else if (level === "info") { + console.info(prefix, message); + } else { + console.debug(prefix, message); + } +} + +export const logger = { + debug: (tag: string, message: string) => log("debug", tag, message), + info: (tag: string, message: string) => log("info", tag, message), + warn: (tag: string, message: string, error?: unknown) => log("warn", tag, message, error), + error: (tag: string, message: string, error?: unknown) => log("error", tag, message, error), +}; diff --git a/packages/app/src/main/windows/mainWindow.ts b/packages/app/src/main/windows/mainWindow.ts index 8211456..bf7f149 100644 --- a/packages/app/src/main/windows/mainWindow.ts +++ b/packages/app/src/main/windows/mainWindow.ts @@ -2,6 +2,7 @@ import { app, BrowserWindow, shell } from "electron"; import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; import { normalizeSafeExternalUrl } from "../utils/externalUrl"; +import { logger } from "../utils/logger"; import { IPC_APP_CHANNELS } from "@codenexus/shared/ipc/channels"; import type { AppWindowState } from "@codenexus/shared/ipc/contracts"; import { resolveUiFontSizeZoomFactor, type UserLocalSettings } from "@codenexus/shared/localSettings"; @@ -64,7 +65,9 @@ export async function createMainWindow(opts: MainWindowOptions): Promise 0) { win.webContents.setZoomFactor(zoomFactor); } - } catch {} + } catch (error) { + logger.warn("window", "failed to set initial zoom factor", error); + } // 禁止渲染进程自行打开新窗口;外链统一交给系统浏览器。 win.webContents.setWindowOpenHandler(({ url }) => { @@ -84,7 +87,9 @@ export async function createMainWindow(opts: MainWindowOptions): Promise ({ isMaximized: win.isMaximized(), @@ -95,7 +100,9 @@ export async function createMainWindow(opts: MainWindowOptions): Promise { try { win.webContents.send(IPC_APP_CHANNELS.appWindowState, toWindowState()); - } catch {} + } catch (error) { + logger.warn("window", "failed to push window state", error); + } }; // 首屏加载完成后同步一次状态;并在窗口状态变化时持续推送。