|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const test = require('node:test'); |
| 4 | +const assert = require('node:assert/strict'); |
| 5 | +const path = require('node:path'); |
| 6 | +const { pathToFileURL } = require('node:url'); |
| 7 | +const { isTrustedUiUrl, isTrustedUiSender } = require('../app/services/ui-origin.cjs'); |
| 8 | + |
| 9 | +const uiFile = path.resolve('synthetic BREEZE~1', '中文 app', 'index.html'); |
| 10 | +const uiUrl = pathToFileURL(uiFile).href; |
| 11 | +const chromiumUrl = uiUrl.replace(/%7e/ig, '~'); |
| 12 | + |
| 13 | +function fixture(url = chromiumUrl) { |
| 14 | + const frame = { url }; |
| 15 | + const contents = { mainFrame: frame, isDestroyed: () => false }; |
| 16 | + const win = { webContents: contents, isDestroyed: () => false }; |
| 17 | + return { win, contents, frame, event: { sender: contents, senderFrame: frame } }; |
| 18 | +} |
| 19 | + |
| 20 | +test('the same UI document accepts both Node and Chromium tilde encodings', () => { |
| 21 | + const encoded = chromiumUrl.replace('~', '%7E'); |
| 22 | + assert.equal(isTrustedUiUrl(chromiumUrl, encoded), true); |
| 23 | + assert.equal(isTrustedUiUrl(encoded, chromiumUrl), true); |
| 24 | + assert.equal(isTrustedUiUrl(encoded.replace('%7E', '%7e'), encoded), true); |
| 25 | + assert.equal(isTrustedUiSender(fixture().event, fixture().win, uiUrl), false); |
| 26 | + const h = fixture(); |
| 27 | + assert.equal(isTrustedUiSender(h.event, h.win, encoded), true); |
| 28 | +}); |
| 29 | + |
| 30 | +test('Windows file path case differences do not reject the same local UI', { skip: process.platform !== 'win32' }, () => { |
| 31 | + const lowerDrive = uiUrl.replace(/^file:\/\/\/[A-Z]:/, value => value.toLowerCase()); |
| 32 | + assert.equal(isTrustedUiUrl(lowerDrive, uiUrl), true); |
| 33 | + assert.equal(isTrustedUiUrl(uiUrl.toUpperCase(), uiUrl), true); |
| 34 | +}); |
| 35 | + |
| 36 | +test('only the exact local UI path is accepted', () => { |
| 37 | + for (const url of [ |
| 38 | + pathToFileURL(path.join(path.dirname(uiFile), 'other.html')).href, |
| 39 | + uiUrl + '.evil', uiUrl.replace('/index.html', '/child/index.html'), |
| 40 | + uiUrl.replace('/index.html', '/child/../../index.html'), |
| 41 | + uiUrl.replace(/%7e/ig, '%257E'), |
| 42 | + uiUrl.replace('/index.html', '%2Findex.html'), |
| 43 | + uiUrl.replace('/index.html', '%5Cindex.html'), |
| 44 | + uiUrl.replace('/index.html', '/%ZZindex.html'), |
| 45 | + uiUrl.replace('/index.html', '/%E0%A4index.html'), |
| 46 | + 'https://example.invalid/index.html', 'data:text/html,UI', |
| 47 | + 'about:blank', 'about:srcdoc', 'file://remote-host/share/index.html', |
| 48 | + '', null, undefined, { href: uiUrl }, |
| 49 | + ]) assert.equal(isTrustedUiUrl(url, uiUrl), false, String(url)); |
| 50 | + for (const suffix of ['?x=1', '#translation', '?', '#']) { |
| 51 | + assert.equal(isTrustedUiUrl(uiUrl + suffix, uiUrl), false, suffix); |
| 52 | + } |
| 53 | +}); |
| 54 | + |
| 55 | +test('reserved characters in actual filenames remain distinct from URL state', () => { |
| 56 | + const name = pathToFileURL(path.resolve('app #1', '100% ready', 'index.html')).href; |
| 57 | + assert.equal(isTrustedUiUrl(name, name), true); |
| 58 | + assert.equal(isTrustedUiUrl(name + '#', name), false); |
| 59 | +}); |
| 60 | + |
| 61 | +test('same-URL subframes and other windows never receive privileged actions', () => { |
| 62 | + const h = fixture(); |
| 63 | + assert.equal(isTrustedUiSender(h.event, h.win, uiUrl), true); |
| 64 | + assert.equal(isTrustedUiSender({ ...h.event, senderFrame: { url: chromiumUrl, parent: h.frame } }, h.win, uiUrl), false); |
| 65 | + assert.equal(isTrustedUiSender({ ...h.event, sender: fixture().contents }, h.win, uiUrl), false); |
| 66 | + for (const senderFrame of [null, undefined]) { |
| 67 | + assert.equal(isTrustedUiSender({ ...h.event, senderFrame }, h.win, uiUrl), false); |
| 68 | + } |
| 69 | + h.frame.url = 'about:srcdoc'; |
| 70 | + assert.equal(isTrustedUiSender(h.event, h.win, uiUrl), false); |
| 71 | +}); |
| 72 | + |
| 73 | +test('closed windows and disposed frame metadata fail closed', () => { |
| 74 | + const h = fixture(); |
| 75 | + assert.equal(isTrustedUiSender(h.event, null, uiUrl), false); |
| 76 | + assert.equal(isTrustedUiSender(null, h.win, uiUrl), false); |
| 77 | + h.win.isDestroyed = () => true; |
| 78 | + assert.equal(isTrustedUiSender(h.event, h.win, uiUrl), false); |
| 79 | + h.win.isDestroyed = () => false; |
| 80 | + h.contents.isDestroyed = () => true; |
| 81 | + assert.equal(isTrustedUiSender(h.event, h.win, uiUrl), false); |
| 82 | + h.contents.isDestroyed = () => false; |
| 83 | + Object.defineProperty(h.frame, 'url', { get() { throw new Error('Frame was disposed'); } }); |
| 84 | + assert.equal(isTrustedUiSender(h.event, h.win, uiUrl), false); |
| 85 | +}); |
0 commit comments