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
59
packages/core/src/url-policy.ts
Normal file
59
packages/core/src/url-policy.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// packages/core/src/url-policy.ts
|
||||
// 외부 URL·내비게이션 정책 SSOT. 순수 함수만 둔다 — Electron 어댑터는 데스크톱 main이 맡는다.
|
||||
//
|
||||
// - 앱 밖으로 넘기는 URL(OS 기본 핸들러)은 허용 scheme만 연다. file:, UNC, ms-*, search-ms: 같은
|
||||
// OS 핸들러는 원격 코드 실행 경로가 되므로 막는다.
|
||||
// - 앱 창은 앱 자신의 오리진 밖으로 이동하지 않는다(원격 페이지가 preload API를 얻지 못하게).
|
||||
|
||||
export interface ExternalUrlPolicyOptions {
|
||||
/** 허용할 scheme(콜론 포함, 소문자). 기본 https:·mailto: */
|
||||
allowSchemes?: readonly string[]
|
||||
/** 추가로 허용할 정확한 오리진(예: 개발용 http://localhost:3000). */
|
||||
allowOrigins?: readonly string[]
|
||||
}
|
||||
|
||||
export const DEFAULT_EXTERNAL_SCHEMES: readonly string[] = ['https:', 'mailto:']
|
||||
|
||||
function parse(url: string): URL | null {
|
||||
if (typeof url !== 'string' || url.trim() === '' || url.length > 8192) return null
|
||||
// 백슬래시로 시작하는 UNC(\\server\share)는 URL 파서가 상대 경로로 보지 않도록 먼저 거른다.
|
||||
if (url.startsWith('\\\\') || url.startsWith('//')) return null
|
||||
try {
|
||||
return new URL(url)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** OS 기본 핸들러(브라우저·메일)로 넘겨도 되는 URL인지. */
|
||||
export function isAllowedExternalUrl(url: string, options: ExternalUrlPolicyOptions = {}): boolean {
|
||||
const parsed = parse(url)
|
||||
if (!parsed) return false
|
||||
const origin = parsed.origin
|
||||
if (options.allowOrigins?.some((allowed) => allowed === origin)) return true
|
||||
const schemes = options.allowSchemes ?? DEFAULT_EXTERNAL_SCHEMES
|
||||
if (!schemes.includes(parsed.protocol.toLowerCase())) return false
|
||||
if (parsed.protocol === 'https:') {
|
||||
// 자격 증명이 박힌 URL(https://user@evil)이나 호스트 없는 URL은 열지 않는다.
|
||||
if (!parsed.hostname || parsed.username || parsed.password) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* URL이 앱 자신의 페이지인지. appOrigins는 'file://' 또는 'http://localhost:5173' 같은 오리진.
|
||||
* file: 은 오리진이 'null'이라 scheme으로 비교한다.
|
||||
*/
|
||||
export function isAppOrigin(url: string, appOrigins: readonly string[]): boolean {
|
||||
const parsed = parse(url)
|
||||
if (!parsed) return false
|
||||
for (const allowed of appOrigins) {
|
||||
if (allowed === 'file://') {
|
||||
if (parsed.protocol === 'file:') return true
|
||||
continue
|
||||
}
|
||||
const allowedParsed = parse(allowed)
|
||||
if (allowedParsed && allowedParsed.origin === parsed.origin) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue