// src/main/windows/web-contents-hardening.ts // 외부 URL·내비게이션 정책(@d3ro/core/url-policy)의 Electron 어댑터. // - 앱 창은 앱 오리진 밖으로 이동하지 않는다(원격 페이지가 preload electronAPI를 얻지 못하게). // - 새 창 요청은 허용 scheme일 때만 OS 브라우저로 넘긴다. // - 모든 shell.openExternal 호출은 openExternalSafe 를 거친다. import { shell, type BrowserWindow, type IpcMainInvokeEvent } from 'electron' import { isAllowedExternalUrl, isAppOrigin, type ExternalUrlPolicyOptions } from '@d3ro/core/url-policy' import { getLogger } from '../services/LoggerService' const logger = getLogger('WebContentsHardening') /** 앱 렌더러가 올라오는 오리진: 배포본 file://, 개발 서버(ELECTRON_RENDERER_URL). */ export function appOrigins(): string[] { const origins = ['file://'] const devUrl = process.env['ELECTRON_RENDERER_URL'] if (devUrl) { try { origins.push(new URL(devUrl).origin) } catch { // 잘못된 개발 URL은 무시 — file:// 만 허용한다. } } return origins } /** 로그에 토큰·쿼리가 남지 않도록 scheme + host 만 남긴다. */ function redact(url: string): string { try { const parsed = new URL(url) return `${parsed.protocol}//${parsed.host}` } catch { return '(unparseable)' } } /** 허용된 URL만 OS 기본 핸들러로 연다. 열었으면 true. */ export async function openExternalSafe(url: string, options: ExternalUrlPolicyOptions = {}): Promise { if (!isAllowedExternalUrl(url, options)) { logger.warn(`Blocked external URL: ${redact(url)}`) return false } await shell.openExternal(url) return true } /** IPC 호출자가 앱 자신의 페이지인지(원격 페이지가 특권 IPC를 쓰지 못하게). */ export function isTrustedIpcSender(event: Pick): boolean { const url = event.senderFrame?.url if (typeof url !== 'string') return false return isAppOrigin(url, appOrigins()) } /** 창의 webContents에 내비게이션·새 창·webview 가드를 건다. */ export function hardenWebContents(win: BrowserWindow): void { const contents = win.webContents const guardNavigation = (event: { preventDefault: () => void }, url: string): void => { if (isAppOrigin(url, appOrigins())) return event.preventDefault() void openExternalSafe(url).catch((err: unknown) => { logger.warn(`openExternal failed: ${err instanceof Error ? err.message : String(err)}`) }) } contents.on('will-navigate', guardNavigation) contents.on('will-redirect', guardNavigation) contents.setWindowOpenHandler((details) => { void openExternalSafe(details.url).catch((err: unknown) => { logger.warn(`openExternal failed: ${err instanceof Error ? err.message : String(err)}`) }) return { action: 'deny' } }) contents.on('will-attach-webview', (event) => { event.preventDefault() }) }