Apply all CODE-AUDIT.md findings (S1-S5, P1-P3, M1-M3)
Security: - S1 (backup.ts): require explicit confirmation before enabling custom-command templates from a backup file; import templates in a disabled state if declined - S2 (cookies.ts): deny non-http/https popups in cookie login window, matching the main window's setWindowOpenHandler defence - S3 (download.ts): main-process concurrency cap — startDownload now rejects spawns when active.size >= maxConcurrent (defence-in-depth; renderer already enforces this but should not be the only gate) - S4 (settings.ts, validation.ts): reject filenameTemplate values containing path traversal (.. segments or absolute paths); validate outputDir is absolute - S5 (history.ts, errorlog.ts, templates.ts, validation.ts): per-field validation on JSON reads — invalid entries dropped rather than blindly trusted Performance: - P1 (settings.ts): getSettings() now only writes to disk when sanitization actually changed a value; removes synchronous file I/O on every hot-path read - P2 (ipc.ts, download.ts, downloads.ts): renderer forwards its pre-probed metadata (title/channel/duration) via StartDownloadOptions.meta; main uses it directly instead of spawning a redundant second yt-dlp probe Maintainability: - M1 (log.ts, download.ts, probe.ts): extract duplicated cleanError() to src/main/log.ts; both callers import from the shared module - M2 (buildArgs.ts): document parseExtraArgs escape-sequence limitations - M3 (electron-builder.yml): clarify icon TODO as a pre-release action - P3 (downloads.ts): document that lowering maxConcurrent mid-flight does not pause active downloads (intended behaviour, now written down) Tests: - Add test/validation.test.ts covering isSafeFilenameTemplate, isSafeOutputDir, isValidHistoryEntry, isValidErrorLogEntry, isTemplateLike (76 tests pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+43
-5
@@ -1,6 +1,7 @@
|
||||
import { app } from 'electron'
|
||||
import { join } from 'path'
|
||||
import Store from 'electron-store'
|
||||
import { isSafeFilenameTemplate, isSafeOutputDir } from './validation'
|
||||
import {
|
||||
AUDIO_FORMATS,
|
||||
VIDEO_CONTAINERS,
|
||||
@@ -90,13 +91,42 @@ function getStore(): Store<Settings> {
|
||||
|
||||
export function getSettings(): Settings {
|
||||
const s = getStore()
|
||||
// Fill in the real Downloads path the first time, and persist it.
|
||||
if (!s.get('outputDir')) s.set('outputDir', app.getPath('downloads'))
|
||||
// Migrate settings files that predate downloadOptions (or hold a partial one).
|
||||
s.set('downloadOptions', sanitizeOptions(s.get('downloadOptions')))
|
||||
// getSettings() is on hot paths (buildCommand, notification checks, the system-
|
||||
// theme bridge, several IPC handlers). electron-store writes to disk on every
|
||||
// `set`, so only write when something actually changed — otherwise this churns
|
||||
// the settings file on every read. (audit P1)
|
||||
const cur = s.store
|
||||
if (!cur.outputDir) {
|
||||
// Fill in the real Downloads path the first time, and persist it once.
|
||||
s.set('outputDir', app.getPath('downloads'))
|
||||
}
|
||||
// Migrate settings files that predate downloadOptions (or hold a partial one),
|
||||
// but only persist when sanitizing actually altered the stored value.
|
||||
const sanitized = sanitizeOptions(cur.downloadOptions)
|
||||
if (!downloadOptionsEqual(sanitized, cur.downloadOptions)) {
|
||||
s.set('downloadOptions', sanitized)
|
||||
}
|
||||
return s.store
|
||||
}
|
||||
|
||||
/** Shallow structural equality for DownloadOptions (sponsorBlockCategories compared by value). */
|
||||
function downloadOptionsEqual(a: DownloadOptions, b: unknown): boolean {
|
||||
if (!b || typeof b !== 'object') return false
|
||||
const o = b as Partial<DownloadOptions>
|
||||
const keys = Object.keys(a) as (keyof DownloadOptions)[]
|
||||
for (const k of keys) {
|
||||
if (k === 'sponsorBlockCategories') {
|
||||
const av = a[k]
|
||||
const bv = o[k]
|
||||
if (!Array.isArray(bv) || av.length !== bv.length) return false
|
||||
for (let i = 0; i < av.length; i++) if (av[i] !== bv[i]) return false
|
||||
} else if (a[k] !== o[k]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Validate each key before persisting — the renderer is the only caller today,
|
||||
// but settings flow into process spawning (maxConcurrent) and the native window
|
||||
// background (theme), so an out-of-range or malformed value shouldn't get stored.
|
||||
@@ -148,9 +178,17 @@ export function setSettings(partial: Partial<Settings>): Settings {
|
||||
s.set('downloadOptions', sanitizeOptions(value))
|
||||
break
|
||||
case 'outputDir':
|
||||
if (typeof value === 'string' && isSafeOutputDir(value.trim())) s.set('outputDir', value.trim())
|
||||
break
|
||||
case 'filenameTemplate':
|
||||
if (typeof value === 'string' && isSafeFilenameTemplate(value.trim())) {
|
||||
s.set('filenameTemplate', value.trim())
|
||||
}
|
||||
break
|
||||
case 'defaultVideoQuality':
|
||||
case 'defaultAudioQuality':
|
||||
case 'filenameTemplate':
|
||||
// proxy may carry plaintext credentials (user:pass@host); they are stored
|
||||
// and exported by exportBackup as-is — documented, not masked.
|
||||
case 'proxy':
|
||||
case 'rateLimit':
|
||||
if (typeof value === 'string') s.set(key, value)
|
||||
|
||||
Reference in New Issue
Block a user