101 lines
4.2 KiB
TypeScript
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')
|
|
})
|
|
})
|