fix(security): trust file: URLs only inside the app renderer directory

This commit is contained in:
Yun Chan 2026-09-28 02:16:18 +09:00
parent b306034bfc
commit 5322d981cc
5 changed files with 299 additions and 17 deletions

View file

@ -0,0 +1,69 @@
// file: 앱 오리진 판정을 렌더러 디렉터리로 제한하는 회귀 테스트(드롭한 파일이 앱 창을 대체하던 문제).
import { describe, expect, it } from 'vitest'
import { isAppNavigationTarget, isAppOrigin, isFileUrlWithinRoot } from '../src/url-policy'
const ROOT = 'file:///C:/Program%20Files/D3RO%20Voice/resources/app.asar/out/renderer/'
const ORIGINS = ['file://', 'http://localhost:5173']
const OPTIONS = { fileRoot: ROOT }
describe('isFileUrlWithinRoot', () => {
it('accepts files under the renderer root (encoding, drive case, hash/query ignored)', () => {
expect(isFileUrlWithinRoot(`${ROOT}index.html#/meetings`, ROOT)).toBe(true)
expect(isFileUrlWithinRoot(`${ROOT}popups/result-popup/index.html?x=1`, ROOT)).toBe(true)
expect(isFileUrlWithinRoot('file:///c:/program files/d3ro voice/resources/app.asar/out/renderer/index.html', ROOT)).toBe(true)
expect(isFileUrlWithinRoot(`${ROOT}index.html`, ROOT.slice(0, -1))).toBe(true)
})
it('accepts a non-ASCII install path encoded the way Chromium encodes it', () => {
const root = 'file:///C:/Users/%EC%9C%A4%EC%B0%AC/AppData/Local/Programs/d3ro-voice/resources/app.asar/out/renderer/'
expect(isFileUrlWithinRoot('file:///C:/Users/윤찬/AppData/Local/Programs/d3ro-voice/resources/app.asar/out/renderer/index.html', root)).toBe(true)
})
it.each([
'file:///C:/Users/me/Downloads/x.html',
'file:///C:/Program%20Files/D3RO%20Voice/resources/app.asar/out/renderer-evil/index.html',
'file:///C:/Program%20Files/D3RO%20Voice/resources/app.asar/out/index.html',
'file://evil-host/share/x.html',
'file://evil-host/C:/Program%20Files/D3RO%20Voice/resources/app.asar/out/renderer/index.html',
`${ROOT}..%2F..%2Fx.html`,
`${ROOT}..%5C..%5Cx.html`,
`${ROOT}%2e%2e/%2e%2e/x.html`,
'http://localhost:5173/index.html',
'not a url',
])('rejects %s', (url) => {
expect(isFileUrlWithinRoot(url, ROOT)).toBe(false)
})
it('rejects everything when the root itself is not a file URL', () => {
expect(isFileUrlWithinRoot(`${ROOT}index.html`, 'https://example.com/')).toBe(false)
})
})
describe('isAppOrigin with fileRoot', () => {
it('trusts only the renderer bundle and the dev server', () => {
expect(isAppOrigin(`${ROOT}index.html#/settings`, ORIGINS, OPTIONS)).toBe(true)
expect(isAppOrigin('http://localhost:5173/#/settings', ORIGINS, OPTIONS)).toBe(true)
expect(isAppOrigin('file:///C:/Users/me/Downloads/evil.html', ORIGINS, OPTIONS)).toBe(false)
expect(isAppOrigin('file://host/share/evil.html', ORIGINS, OPTIONS)).toBe(false)
expect(isAppOrigin('https://evil.example/', ORIGINS, OPTIONS)).toBe(false)
})
it('keeps the dev server origin exact even with a fileRoot', () => {
expect(isAppOrigin('http://localhost:5174/', ORIGINS, OPTIONS)).toBe(false)
})
})
describe('isAppNavigationTarget', () => {
it('allows app HTML pages and dev server routes', () => {
expect(isAppNavigationTarget(`${ROOT}index.html`, ORIGINS, OPTIONS)).toBe(true)
expect(isAppNavigationTarget(`${ROOT}popups/command-popup/index.html#x`, ORIGINS, OPTIONS)).toBe(true)
expect(isAppNavigationTarget('http://localhost:5173/popups/result-popup/index.html', ORIGINS, OPTIONS)).toBe(true)
})
it('blocks dropped files and non-HTML files inside the bundle', () => {
expect(isAppNavigationTarget('file:///C:/Users/me/Music/meeting.mp3', ORIGINS, OPTIONS)).toBe(false)
expect(isAppNavigationTarget('file:///C:/Users/me/Downloads/x.html', ORIGINS, OPTIONS)).toBe(false)
expect(isAppNavigationTarget(`${ROOT}assets/index-abc.js`, ORIGINS, OPTIONS)).toBe(false)
expect(isAppNavigationTarget(`${ROOT}`, ORIGINS, OPTIONS)).toBe(false)
})
})