91 lines
3.7 KiB
TypeScript
91 lines
3.7 KiB
TypeScript
// 드롭한 로컬 파일(file:)이 앱 창을 대체하거나 신뢰 IPC 발신자로 인정되던 문제의 회귀 테스트.
|
|
// 앱 페이지는 렌더러 디렉터리(join(__dirname, '../renderer')) 아래 HTML만이다.
|
|
|
|
import { resolve } from 'path'
|
|
import { pathToFileURL } from 'url'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { shell } from 'electron'
|
|
import {
|
|
appRendererFileRoot,
|
|
hardenWebContents,
|
|
isAllowedAppNavigation,
|
|
isTrustedIpcSender,
|
|
} from '../../../src/main/windows/web-contents-hardening'
|
|
|
|
type Handler = (...args: unknown[]) => unknown
|
|
|
|
// 테스트에서 어댑터 모듈의 __dirname 은 src/main/windows 이므로 렌더러 루트는 src/main/renderer.
|
|
const RENDERER_ROOT = `${pathToFileURL(resolve(__dirname, '../../../src/main/renderer')).href}/`
|
|
|
|
function fakeWindow() {
|
|
const listeners = new Map<string, Handler>()
|
|
const webContents = {
|
|
on: vi.fn((event: string, listener: Handler) => {
|
|
listeners.set(event, listener)
|
|
}),
|
|
setWindowOpenHandler: vi.fn(),
|
|
}
|
|
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
|
|
},
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(shell.openExternal).mockClear()
|
|
})
|
|
|
|
describe('appRendererFileRoot', () => {
|
|
it('is the renderer directory next to the main bundle, as a file URL ending in /', () => {
|
|
expect(appRendererFileRoot()).toBe(RENDERER_ROOT)
|
|
})
|
|
})
|
|
|
|
describe('file: navigation guard', () => {
|
|
it('blocks a dropped HTML/audio file from replacing the app window and never opens it externally', async () => {
|
|
const w = fakeWindow()
|
|
hardenWebContents(w.win)
|
|
expect(w.fire('will-navigate', 'file:///C:/Users/me/Downloads/evil.html')).toHaveBeenCalled()
|
|
expect(w.fire('will-navigate', 'file:///C:/Users/me/Music/meeting.mp3')).toHaveBeenCalled()
|
|
expect(w.fire('will-navigate', 'file://host/share/evil.html')).toHaveBeenCalled()
|
|
expect(w.fire('will-redirect', 'file:///C:/Users/me/Downloads/evil.html')).toHaveBeenCalled()
|
|
await new Promise((r) => setTimeout(r, 10))
|
|
expect(shell.openExternal).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('still allows the app own renderer pages', () => {
|
|
const w = fakeWindow()
|
|
hardenWebContents(w.win)
|
|
expect(w.fire('will-navigate', `${RENDERER_ROOT}index.html#/meetings`)).not.toHaveBeenCalled()
|
|
expect(w.fire('will-navigate', `${RENDERER_ROOT}popups/result-popup/index.html`)).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not navigate to non-HTML files inside the bundle', () => {
|
|
expect(isAllowedAppNavigation(`${RENDERER_ROOT}assets/index.js`)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isTrustedIpcSender with file: senders', () => {
|
|
it('trusts the app renderer but not arbitrary local or UNC files', () => {
|
|
expect(isTrustedIpcSender({ senderFrame: { url: `${RENDERER_ROOT}index.html` } } as never)).toBe(true)
|
|
expect(isTrustedIpcSender({ senderFrame: { url: 'file:///C:/Users/me/Downloads/evil.html' } } as never)).toBe(false)
|
|
expect(isTrustedIpcSender({ senderFrame: { url: 'file://host/share/evil.html' } } as never)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('dev server origin', () => {
|
|
it('keeps allowing ELECTRON_RENDERER_URL routes', () => {
|
|
vi.stubEnv('ELECTRON_RENDERER_URL', 'http://localhost:5173')
|
|
try {
|
|
expect(isAllowedAppNavigation('http://localhost:5173/popups/result-popup/index.html')).toBe(true)
|
|
expect(isTrustedIpcSender({ senderFrame: { url: 'http://localhost:5173/#/x' } } as never)).toBe(true)
|
|
expect(isAllowedAppNavigation('file:///C:/Users/me/Downloads/evil.html')).toBe(false)
|
|
} finally {
|
|
vi.unstubAllEnvs()
|
|
}
|
|
})
|
|
})
|