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
75
apps/desktop/src/main/windows/web-contents-hardening.ts
Normal file
75
apps/desktop/src/main/windows/web-contents-hardening.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// 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()
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue