fix(main): SFTP data integrity, session lifecycle, security hardening

- upload: per-chunk buffer (ssh2 re-reads the overflow tail after the ACK;
  a reused buffer silently corrupted every file >= ~254KB)
- close the cached SFTP channel on eviction, attach an 'error' handler,
  close the download handle, time out execQuiet, fail partial deletes
- per-session StringDecoder for the ssh data plane (CJK mojibake), real
  exit codes, safe replay truncation, zmodem abort/counter/timer fixes
- sysinfo: idempotent poll end, error routing, per-poll watchdog, proc(5)
  CPU total; expand cd ~/$HOME/%USERPROFILE% paths; log sanitizer fixes
- security: will-navigate guard, central IPC sender check, scheme
  allowlist for openExternal, single-instance else branch, layout id and
  log-name whitelists, custom theme sanitizing, atomic JSON writes with
  EPERM retry in store.writeJson
- updater: per-attempt feed choice, quitAndInstall relaunch, dev guard,
  update-state getter
This commit is contained in:
Bill committed 2026-09-20 20:26:34 +08:00
1 parent 4c6a29ef28
commit e04f4f0ac1
16 files changed
+452 -98

No files matched your search

+19 -2
View File
@@ -1,7 +1,13 @@
import { app } from 'electron'
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'
import { mkdirSync, readFileSync, readdirSync, rmSync } from 'fs'
import { join } from 'path'
import type { LayoutMeta } from '../shared/ipc'
import { writeJson } from './store'
/** Layout ids reach this module straight from the renderer and are joined into
* a path, so only a plain id is accepted — `..`, separators and drive prefixes
* would otherwise escape `userData/layouts`. */
const LAYOUT_ID = /^[A-Za-z0-9._-]{1,64}$/
const layoutsDir = (): string => {
const dir = join(app.getPath('userData'), 'layouts')
@@ -9,6 +15,10 @@ const layoutsDir = (): string => {
return dir
}
function isValidLayoutId(id: unknown): id is string {
return typeof id === 'string' && LAYOUT_ID.test(id)
}
function layoutPath(id: string): string {
return join(layoutsDir(), `${id}.json`)
}
@@ -39,6 +49,7 @@ export function listLayouts(): LayoutMeta[] {
}
export function getLayout(id: string): string | null {
if (!isValidLayoutId(id)) return null
try {
const file = JSON.parse(readFileSync(layoutPath(id), 'utf8')) as Partial<LayoutFile>
return typeof file.json === 'string' ? file.json : null
@@ -48,11 +59,17 @@ export function getLayout(id: string): string | null {
}
export function saveLayout(meta: LayoutMeta, json: string): void {
if (!isValidLayoutId(meta.id)) {
throw new Error(`[layouts] invalid layout id: ${String(meta.id)}`)
}
const file: LayoutFile = { id: meta.id, name: meta.name, createdAt: meta.createdAt, json }
writeFileSync(layoutPath(meta.id), JSON.stringify(file, null, 2), 'utf8')
writeJson(layoutPath(meta.id), file)
}
export function deleteLayout(id: string): void {
if (!isValidLayoutId(id)) {
throw new Error(`[layouts] invalid layout id: ${String(id)}`)
}
try {
rmSync(layoutPath(id))
} catch (err) {