d3ro-voice/apps/desktop/tests/main/windows/web-contents-hardening-redteam-r1-0.test.ts

101 lines
4.2 KiB
TypeScript

// 내비게이션·외부 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<string, Handler>()
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<typeof hardenWebContents>[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<string, Handler>()
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')
})
})