security: harden command exec, IPC, deep-link, cookies, and persistence
Seven-tier security audit of the main process, each finding fixed with a
regression test. Typecheck (node + web) clean; unit tests 106 -> 140.
- Tier 1 (command exec/argv): allowlist the yt-dlp --update-to channel
(blocks arbitrary-binary-install RCE); gate per-download extraArgs behind
the customCommandEnabled consent flag in main (blocks --exec RCE); resolve
taskkill/schtasks by absolute System32 path; validate per-download
outputDir; normalize the URL in assertHttpUrl and use it at every spawn.
- Tier 2 (input validation): fix isSafeFilenameTemplate drive-relative
('C:foo') and Windows dotted-'..' traversal bypasses; percent-encode the
untrusted id in entryUrl; catch reserved device names with extensions in
sanitizeDirSegment.
- Tier 3 (fs/backup): drop malformed template rows in importBackup;
normalize deep-link URLs via assertHttpUrl.
- Tier 4 (cookies): confine the sign-in window's navigations/popups to web
URLs (recursively) and deny all web permissions on its session.
- Tier 5 (deep-link/argv): bound the .url file read to 64 KB; match the
aerofetch:// scheme case-insensitively.
- Tier 6 (Electron window): deny camera/mic/geolocation/USB/HID/serial/
Bluetooth permissions on the app window.
- Tier 7 (network/persistence): restrict the watched-source RSS fetch to
youtube.com feed URLs (SSRF guard); complete isValidSource validation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+66
-2
@@ -4,9 +4,10 @@ import {
|
||||
isSafeOutputDir,
|
||||
isValidHistoryEntry,
|
||||
isValidErrorLogEntry,
|
||||
isTemplateLike
|
||||
isTemplateLike,
|
||||
isValidSource
|
||||
} from '../src/main/validation'
|
||||
import type { HistoryEntry, ErrorLogEntry } from '@shared/ipc'
|
||||
import type { HistoryEntry, ErrorLogEntry, Source } from '@shared/ipc'
|
||||
|
||||
// --- S4: filename template path-traversal -----------------------------------
|
||||
|
||||
@@ -40,6 +41,24 @@ describe('isSafeFilenameTemplate', () => {
|
||||
it('allows .. only when embedded in a longer segment (not a real traversal)', () => {
|
||||
// '..foo' / 'foo..bar' are filenames, not parent-dir references.
|
||||
expect(isSafeFilenameTemplate('my..video.%(ext)s')).toBe(true)
|
||||
expect(isSafeFilenameTemplate('.%(title)s.%(ext)s')).toBe(true) // leading dot = hidden file
|
||||
})
|
||||
|
||||
// --- T1: Windows-specific bypasses --------------------------------------
|
||||
|
||||
it('rejects a drive-relative prefix that path.isAbsolute misses', () => {
|
||||
// 'C:foo' is NOT absolute per Node, but still escapes the output dir.
|
||||
expect(isSafeFilenameTemplate('C:foo\\%(title)s.%(ext)s')).toBe(false)
|
||||
expect(isSafeFilenameTemplate('c:%(title)s')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a .. segment dressed up with trailing/leading dots or spaces', () => {
|
||||
// Windows trims trailing dots/spaces, so these all resolve to '..'.
|
||||
expect(isSafeFilenameTemplate('%(title)s/.. /x')).toBe(false)
|
||||
expect(isSafeFilenameTemplate('%(title)s/.. ./x')).toBe(false)
|
||||
expect(isSafeFilenameTemplate('%(title)s/ ../x')).toBe(false)
|
||||
expect(isSafeFilenameTemplate('%(title)s\\.. \\x')).toBe(false)
|
||||
expect(isSafeFilenameTemplate('...')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -144,3 +163,48 @@ describe('isTemplateLike', () => {
|
||||
expect(isTemplateLike('str')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// --- T7: source row validation (feedUrl drives a network fetch) --------------
|
||||
|
||||
const validSource: Source = {
|
||||
id: 's1',
|
||||
url: 'https://www.youtube.com/@x',
|
||||
kind: 'channel',
|
||||
title: 'X',
|
||||
addedAt: 1700000000000,
|
||||
itemCount: 5
|
||||
}
|
||||
|
||||
describe('isValidSource', () => {
|
||||
it('accepts a well-formed source and its optional fields', () => {
|
||||
expect(isValidSource(validSource)).toBe(true)
|
||||
expect(
|
||||
isValidSource({
|
||||
...validSource,
|
||||
channel: 'X',
|
||||
watched: true,
|
||||
feedUrl: 'https://www.youtube.com/feeds/videos.xml?channel_id=UC',
|
||||
lastIndexedAt: 1700000000001
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a non-string feedUrl (audit T7)', () => {
|
||||
expect(isValidSource({ ...validSource, feedUrl: 123 })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a non-boolean watched (audit T7)', () => {
|
||||
expect(isValidSource({ ...validSource, watched: 'yes' })).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a wrong kind or a missing required field', () => {
|
||||
expect(isValidSource({ ...validSource, kind: 'video' })).toBe(false)
|
||||
const { id: _id, ...noId } = validSource
|
||||
expect(isValidSource(noId)).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects non-objects', () => {
|
||||
expect(isValidSource(null)).toBe(false)
|
||||
expect(isValidSource('nope')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user