75 lines
2.9 KiB
TypeScript
75 lines
2.9 KiB
TypeScript
// src/main/windows/web-contents-hardening.ts
|
|
// 외부 URL·내비게이션 정책(@d3ro/core/url-policy)의 Electron 어댑터.
|
|
// - 앱 창은 앱 오리진 밖으로 이동하지 않는다(원격 페이지가 preload electronAPI를 얻지 못하게).
|
|
// - 새 창 요청은 허용 scheme일 때만 OS 브라우저로 넘긴다.
|
|
// - 모든 shell.openExternal 호출은 openExternalSafe 를 거친다.
|
|
|
|
import { shell, type BrowserWindow, type IpcMainInvokeEvent } from 'electron'
|
|
import { isAllowedExternalUrl, isAppOrigin, type ExternalUrlPolicyOptions } from '@d3ro/core/url-policy'
|
|
import { getLogger } from '../services/LoggerService'
|
|
|
|
const logger = getLogger('WebContentsHardening')
|
|
|
|
/** 앱 렌더러가 올라오는 오리진: 배포본 file://, 개발 서버(ELECTRON_RENDERER_URL). */
|
|
export function appOrigins(): string[] {
|
|
const origins = ['file://']
|
|
const devUrl = process.env['ELECTRON_RENDERER_URL']
|
|
if (devUrl) {
|
|
try {
|
|
origins.push(new URL(devUrl).origin)
|
|
} catch {
|
|
// 잘못된 개발 URL은 무시 — file:// 만 허용한다.
|
|
}
|
|
}
|
|
return origins
|
|
}
|
|
|
|
/** 로그에 토큰·쿼리가 남지 않도록 scheme + host 만 남긴다. */
|
|
function redact(url: string): string {
|
|
try {
|
|
const parsed = new URL(url)
|
|
return `${parsed.protocol}//${parsed.host}`
|
|
} catch {
|
|
return '(unparseable)'
|
|
}
|
|
}
|
|
|
|
/** 허용된 URL만 OS 기본 핸들러로 연다. 열었으면 true. */
|
|
export async function openExternalSafe(url: string, options: ExternalUrlPolicyOptions = {}): Promise<boolean> {
|
|
if (!isAllowedExternalUrl(url, options)) {
|
|
logger.warn(`Blocked external URL: ${redact(url)}`)
|
|
return false
|
|
}
|
|
await shell.openExternal(url)
|
|
return true
|
|
}
|
|
|
|
/** IPC 호출자가 앱 자신의 페이지인지(원격 페이지가 특권 IPC를 쓰지 못하게). */
|
|
export function isTrustedIpcSender(event: Pick<IpcMainInvokeEvent, 'senderFrame'>): boolean {
|
|
const url = event.senderFrame?.url
|
|
if (typeof url !== 'string') return false
|
|
return isAppOrigin(url, appOrigins())
|
|
}
|
|
|
|
/** 창의 webContents에 내비게이션·새 창·webview 가드를 건다. */
|
|
export function hardenWebContents(win: BrowserWindow): void {
|
|
const contents = win.webContents
|
|
const guardNavigation = (event: { preventDefault: () => void }, url: string): void => {
|
|
if (isAppOrigin(url, appOrigins())) return
|
|
event.preventDefault()
|
|
void openExternalSafe(url).catch((err: unknown) => {
|
|
logger.warn(`openExternal failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
})
|
|
}
|
|
contents.on('will-navigate', guardNavigation)
|
|
contents.on('will-redirect', guardNavigation)
|
|
contents.setWindowOpenHandler((details) => {
|
|
void openExternalSafe(details.url).catch((err: unknown) => {
|
|
logger.warn(`openExternal failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
})
|
|
return { action: 'deny' }
|
|
})
|
|
contents.on('will-attach-webview', (event) => {
|
|
event.preventDefault()
|
|
})
|
|
}
|