security(main): dialog-grant admission for local paths, explicit webPreferences

New localPathGrants.ts: an in-memory registry of paths the user picked in a
native dialog. Every check resolves realpath + stat at grant and use time;
Windows case folding; missing files, directories-as-files, devices and
symlinked parents can never pass. SFTP upload/download and zmodem send/
receive now refuse renderer-supplied local paths that were never granted,
returning the resolved path so callers never re-traverse a symlink. keyPath
is validated as a regular file <= 1MB before reading (a device node would
have blocked the UI thread forever). Tests inject a stub policy via
setLocalPathPolicy; production always defaults to the real registry.

Also: webPreferences now explicitly pins contextIsolation/nodeIntegration/
webSecurity instead of relying on defaults.

New offline test tests/local-path-grants.mjs (37 assertions incl. symlink
escape); offline suite grows to 13. i18n: 6 main.sftp/main.key error keys
in 4 languages.
This commit is contained in:
Bill committed 2026-10-07 22:06:45 +08:00
1 parent 9c75cdc7d4
commit d22923aedd
14 files changed
+612 -18

No files matched your search

+6
View File
@@ -26,6 +26,8 @@ const main: Record<string, string> = {
'main.ssh.shellOpenFailed': 'Could not open the SSH shell ({host}:{port}): {detail}',
'main.ssh.initFailed': 'SSH connection initialization failed ({host}:{port}): {detail}',
'main.ssh.readKeyFailed': 'Could not read the private key file {path}: {detail}',
'main.ssh.keyNotAFile': 'The private key path is not a regular file (directories, devices and other special files are refused)',
'main.ssh.keyTooLarge': 'The private key file is too large - the path is probably wrong',
'main.sftp.sessionGone': 'The SSH session does not exist or has disconnected',
'main.sftp.deleteFailed': 'Delete failed: {detail}',
@@ -35,6 +37,7 @@ const main: Record<string, string> = {
'main.sftp.openTimeout': 'Opening the SFTP channel timed out',
'main.sftp.commandExitCode': 'Command exited with code {code}',
'main.sftp.cancelled': 'Cancelled',
'main.sftp.localNotAllowed': 'Local path not authorised: {path}. Pick it again with the "choose file / choose directory" dialog.',
'main.zmodem.transferFailed': 'Transfer failed',
'main.zmodem.transferTimeout': 'Transfer timed out',
@@ -49,6 +52,9 @@ const main: Record<string, string> = {
'main.zmodem.sessionCreateFailed': 'Could not establish the ZMODEM session',
'main.zmodem.noSaveDir': 'No save directory specified',
'main.zmodem.noFiles': 'No files selected',
'main.zmodem.saveDirNotAllowed': 'The save directory is not authorised. Pick it again with the "choose directory" dialog.',
'main.zmodem.savePathNotAllowed': 'The remote file name was refused, transfer aborted: {name}',
'main.zmodem.filesNotAllowed': 'The selected local files are not authorised, transfer aborted',
'main.ipc.connectionMissing': 'Connection bookmark not found ({id})',
+6
View File
@@ -26,6 +26,8 @@ const main: Record<string, string> = {
'main.ssh.shellOpenFailed': 'SSH シェルを開けません ({host}:{port}): {detail}',
'main.ssh.initFailed': 'SSH 接続の初期化に失敗しました ({host}:{port}): {detail}',
'main.ssh.readKeyFailed': '秘密鍵ファイルを読み取れません {path}: {detail}',
'main.ssh.keyNotAFile': '秘密鍵のパスが通常のファイルではありません(ディレクトリ・デバイス・その他の特殊ファイルは受け付けません)',
'main.ssh.keyTooLarge': '秘密鍵ファイルが大きすぎます。パスの指定が誤っている可能性があります',
'main.sftp.sessionGone': 'SSH セッションが存在しないか、切断されています',
'main.sftp.deleteFailed': '削除に失敗しました: {detail}',
@@ -35,6 +37,7 @@ const main: Record<string, string> = {
'main.sftp.openTimeout': 'SFTP チャネルのオープンがタイムアウトしました',
'main.sftp.commandExitCode': 'コマンドの終了コード {code}',
'main.sftp.cancelled': 'キャンセルしました',
'main.sftp.localNotAllowed': 'ローカルパスが許可されていません: {path}。「ファイルを選択 / ディレクトリを選択」ダイアログで選び直してください。',
'main.zmodem.transferFailed': '転送に失敗しました',
'main.zmodem.transferTimeout': '転送がタイムアウトしました',
@@ -49,6 +52,9 @@ const main: Record<string, string> = {
'main.zmodem.sessionCreateFailed': 'ZMODEM セッションを確立できません',
'main.zmodem.noSaveDir': '保存先ディレクトリが指定されていません',
'main.zmodem.noFiles': 'ファイルが選択されていません',
'main.zmodem.saveDirNotAllowed': '保存先ディレクトリが許可されていません。「ディレクトリを選択」ダイアログで選び直してください。',
'main.zmodem.savePathNotAllowed': 'リモートのファイル名を受け付けられないため、転送を中止しました: {name}',
'main.zmodem.filesNotAllowed': '選択されたローカルファイルが許可されていないため、転送を中止しました',
'main.ipc.connectionMissing': '接続ブックマークが見つかりません ({id})',
+6
View File
@@ -26,6 +26,8 @@ const main: Record<string, string> = {
'main.ssh.shellOpenFailed': '无法打开 SSH shell ({host}:{port}): {detail}',
'main.ssh.initFailed': 'SSH 连接初始化失败 ({host}:{port}): {detail}',
'main.ssh.readKeyFailed': '无法读取私钥文件 {path}: {detail}',
'main.ssh.keyNotAFile': '私钥路径不是一个普通文件(目录、设备或其他特殊文件均不被接受)',
'main.ssh.keyTooLarge': '私钥文件过大,疑似路径填写有误',
'main.sftp.sessionGone': 'SSH 会话不存在或已断开',
'main.sftp.deleteFailed': '删除失败: {detail}',
@@ -35,6 +37,7 @@ const main: Record<string, string> = {
'main.sftp.openTimeout': 'SFTP 通道打开超时',
'main.sftp.commandExitCode': '命令退出码 {code}',
'main.sftp.cancelled': '已取消',
'main.sftp.localNotAllowed': '本地路径未被授权: {path}。请通过「选择文件 / 选择目录」对话框重新选择。',
'main.zmodem.transferFailed': '传输失败',
'main.zmodem.transferTimeout': '传输超时',
@@ -49,6 +52,9 @@ const main: Record<string, string> = {
'main.zmodem.sessionCreateFailed': '无法建立 ZMODEM 会话',
'main.zmodem.noSaveDir': '未指定保存目录',
'main.zmodem.noFiles': '未选择文件',
'main.zmodem.saveDirNotAllowed': '保存目录未被授权,请通过「选择目录」对话框重新选择',
'main.zmodem.savePathNotAllowed': '远端文件名不被接受,已中止传输: {name}',
'main.zmodem.filesNotAllowed': '所选的本地文件未被授权,已中止传输',
'main.ipc.connectionMissing': '连接书签不存在 ({id})',
+6
View File
@@ -26,6 +26,8 @@ const main: Record<string, string> = {
'main.ssh.shellOpenFailed': '無法開啟 SSH shell ({host}:{port}): {detail}',
'main.ssh.initFailed': 'SSH 連線初始化失敗 ({host}:{port}): {detail}',
'main.ssh.readKeyFailed': '無法讀取私密金鑰檔案 {path}: {detail}',
'main.ssh.keyNotAFile': '私密金鑰路徑不是一般檔案(目錄、裝置或其他特殊檔案均不接受)',
'main.ssh.keyTooLarge': '私密金鑰檔案過大,路徑可能填寫有誤',
'main.sftp.sessionGone': 'SSH 連線不存在或已中斷',
'main.sftp.deleteFailed': '刪除失敗: {detail}',
@@ -35,6 +37,7 @@ const main: Record<string, string> = {
'main.sftp.openTimeout': 'SFTP 通道開啟逾時',
'main.sftp.commandExitCode': '指令結束碼 {code}',
'main.sftp.cancelled': '已取消',
'main.sftp.localNotAllowed': '本機路徑未獲授權: {path}。請改用「選擇檔案 / 選擇目錄」對話框重新選擇。',
'main.zmodem.transferFailed': '傳輸失敗',
'main.zmodem.transferTimeout': '傳輸逾時',
@@ -49,6 +52,9 @@ const main: Record<string, string> = {
'main.zmodem.sessionCreateFailed': '無法建立 ZMODEM 連線',
'main.zmodem.noSaveDir': '未指定儲存目錄',
'main.zmodem.noFiles': '未選擇檔案',
'main.zmodem.saveDirNotAllowed': '儲存目錄未獲授權,請改用「選擇目錄」對話框重新選擇',
'main.zmodem.savePathNotAllowed': '遠端檔案名稱不被接受,已中止傳輸: {name}',
'main.zmodem.filesNotAllowed': '所選的本機檔案未獲授權,已中止傳輸',
'main.ipc.connectionMissing': '連線書籤不存在 ({id})',