fix(sync): bind sync engine to its user DB, scope instructions/templates per account, harden navigation
This commit is contained in:
parent
1b8fe445f3
commit
9aa7302944
30 changed files with 2614 additions and 386 deletions
|
|
@ -0,0 +1,100 @@
|
|||
// 내비게이션·외부 URL 가드 회귀 테스트
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ipcMain, shell } from 'electron'
|
||||
import { IPC_CHANNELS } from '@d3ro/core/ipc-channels'
|
||||
import {
|
||||
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', 'file:///C:/app/out/renderer/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: 'file:///C:/app/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: 'file:///C:/app/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')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue