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:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Pure, dependency-free validators for untrusted input that crosses a trust
|
||||
* boundary (renderer writes, backup files, hand-edited JSON on disk).
|
||||
*
|
||||
* Like buildArgs.ts, this module imports nothing from electron/node beyond the
|
||||
* `path` builtin, so it can be unit-tested without spinning up Electron.
|
||||
*/
|
||||
|
||||
import { isAbsolute } from 'path'
|
||||
import type { HistoryEntry, ErrorLogEntry, CommandTemplate } from '@shared/ipc'
|
||||
|
||||
// --- Path-traversal sanitization (audit S4) ---------------------------------
|
||||
|
||||
/**
|
||||
* A filenameTemplate is joined onto the output directory and handed to yt-dlp's
|
||||
* `-o`. Reject anything that could write outside that directory — an absolute
|
||||
* path, or any `..` path segment — so a malicious backup/settings write can't
|
||||
* traverse out of the chosen folder (e.g. '%(title)s\..\..\win32.exe'). The
|
||||
* template still legitimately contains yt-dlp `%(field)s` tokens and `/` or `\`
|
||||
* for sub-folders, which are fine.
|
||||
*/
|
||||
export function isSafeFilenameTemplate(template: string): boolean {
|
||||
if (isAbsolute(template)) return false
|
||||
return !template.split(/[\\/]/).some((segment) => segment === '..')
|
||||
}
|
||||
|
||||
/** An output directory must be an absolute path ('' resolves to OS Downloads). */
|
||||
export function isSafeOutputDir(dir: string): boolean {
|
||||
return dir === '' || isAbsolute(dir)
|
||||
}
|
||||
|
||||
// --- Persisted-JSON entry validation (audit S5) -----------------------------
|
||||
|
||||
const isOptionalString = (v: unknown): boolean => v === undefined || typeof v === 'string'
|
||||
|
||||
/** A persisted history.json row must have the right shape or it's dropped on read. */
|
||||
export function isValidHistoryEntry(o: unknown): o is HistoryEntry {
|
||||
if (!o || typeof o !== 'object') return false
|
||||
const e = o as Record<string, unknown>
|
||||
return (
|
||||
typeof e.id === 'string' &&
|
||||
typeof e.title === 'string' &&
|
||||
typeof e.url === 'string' &&
|
||||
(e.kind === 'video' || e.kind === 'audio') &&
|
||||
typeof e.quality === 'string' &&
|
||||
typeof e.completedAt === 'number' &&
|
||||
isOptionalString(e.filePath) &&
|
||||
isOptionalString(e.channel) &&
|
||||
isOptionalString(e.sizeLabel) &&
|
||||
isOptionalString(e.thumbnail)
|
||||
)
|
||||
}
|
||||
|
||||
/** A persisted errorlog.json row must have the right shape or it's dropped on read. */
|
||||
export function isValidErrorLogEntry(o: unknown): o is ErrorLogEntry {
|
||||
if (!o || typeof o !== 'object') return false
|
||||
const e = o as Record<string, unknown>
|
||||
return (
|
||||
typeof e.id === 'string' &&
|
||||
typeof e.url === 'string' &&
|
||||
(e.kind === 'video' || e.kind === 'audio') &&
|
||||
typeof e.error === 'string' &&
|
||||
typeof e.occurredAt === 'number' &&
|
||||
isOptionalString(e.title)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A persisted templates.json row must at least be an object carrying an id we
|
||||
* can stringify; name/args are then coerced by templates.ts's sanitize(), so a
|
||||
* weak shape check here plus normalisation there is enough.
|
||||
*/
|
||||
export function isTemplateLike(o: unknown): o is CommandTemplate {
|
||||
if (!o || typeof o !== 'object') return false
|
||||
const t = o as Record<string, unknown>
|
||||
return typeof t.id === 'string' || typeof t.id === 'number'
|
||||
}
|
||||
Reference in New Issue
Block a user