// 내비게이션·외부 URL 가드 회귀 테스트 import { beforeEach, describe, expect, it, vi } from 'vitest' import { ipcMain, shell } from 'electron' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { appRendererFileRoot, hardenWebContents, isTrustedIpcSender, openExternalSafe, } from '../../../src/main/windows/web-contents-hardening' type Handler = (...args: unknown[]) => unknown function fakeWindow() { const listeners = new Map() let openHandler: ((details: { url: string }) => { action: string }) | null = null const webContents = { on: vi.fn((event: string, listener: Handler) => { listeners.set(event, listener) }), setWindowOpenHandler: vi.fn((handler: (details: { url: string }) => { action: string }) => { openHandler = handler }), } return { win: { webContents } as unknown as Parameters[0], fire: (event: string, url: string) => { const preventDefault = vi.fn() listeners.get(event)?.({ preventDefault }, url) return preventDefault }, open: (url: string) => openHandler?.({ url }), listeners, } } beforeEach(() => { vi.mocked(shell.openExternal).mockClear() }) describe('hardenWebContents', () => { it('앱 창이 원격 페이지로 이동하지 못하게 막고 https 는 브라우저로 넘긴다', async () => { const w = fakeWindow() hardenWebContents(w.win) const prevented = w.fire('will-navigate', 'https://evil.example/phish') expect(prevented).toHaveBeenCalled() await vi.waitFor(() => expect(shell.openExternal).toHaveBeenCalledWith('https://evil.example/phish')) }) it('앱 자신의 페이지(file://)로의 이동은 허용한다', () => { const w = fakeWindow() hardenWebContents(w.win) expect(w.fire('will-navigate', `${appRendererFileRoot()}index.html#/x`)).not.toHaveBeenCalled() }) it('새 창은 항상 거부하고 file:/UNC/사용자 정의 scheme 은 OS로 넘기지 않는다', async () => { const w = fakeWindow() hardenWebContents(w.win) expect(w.open('file:///C:/Windows/System32/calc.exe')).toEqual({ action: 'deny' }) expect(w.open('search-ms:query=x')).toEqual({ action: 'deny' }) expect(w.fire('will-navigate', 'ms-msdt:/id PCWDiagnostic')).toHaveBeenCalled() await new Promise((r) => setTimeout(r, 10)) expect(shell.openExternal).not.toHaveBeenCalled() expect(w.listeners.has('will-attach-webview')).toBe(true) }) }) describe('openExternalSafe / isTrustedIpcSender', () => { it('허용 scheme 만 연다', async () => { expect(await openExternalSafe('file:///etc/passwd')).toBe(false) expect(await openExternalSafe('https://d3ro.chanpaca.net/')).toBe(true) expect(shell.openExternal).toHaveBeenCalledTimes(1) }) it('원격 페이지가 보낸 IPC 는 신뢰하지 않는다', () => { expect(isTrustedIpcSender({ senderFrame: { url: `${appRendererFileRoot()}index.html` } } as never)).toBe(true) expect(isTrustedIpcSender({ senderFrame: { url: 'https://evil.example/' } } as never)).toBe(false) expect(isTrustedIpcSender({ senderFrame: null } as never)).toBe(false) }) }) describe('SYSTEM.OPEN_EXTERNAL IPC', () => { it('file:·원격 발신자 요청을 거부하고 https 는 연다', async () => { const handlers = new Map() vi.mocked(ipcMain.handle).mockImplementation((channel: string, handler: Handler) => { handlers.set(channel, handler) }) const { registerWindowHandlers } = await import('../../../src/main/ipc/window-handlers') registerWindowHandlers() const handler = handlers.get(IPC_CHANNELS.SYSTEM.OPEN_EXTERNAL)! const appEvent = { senderFrame: { url: `${appRendererFileRoot()}index.html` } } const remoteEvent = { senderFrame: { url: 'https://evil.example/' } } await expect(handler(appEvent, { url: 'file:///C:/Windows/System32/calc.exe' })).resolves.toMatchObject({ success: false }) await expect(handler(remoteEvent, { url: 'https://example.com' })).resolves.toMatchObject({ success: false }) expect(shell.openExternal).not.toHaveBeenCalled() await expect(handler(appEvent, { url: 'https://example.com' })).resolves.toMatchObject({ success: true }) expect(shell.openExternal).toHaveBeenCalledWith('https://example.com') }) })