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:
@@ -90,6 +90,25 @@ function getSystemThemeInfo(): SystemThemeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
// Web permissions a download manager never needs. They're denied for the app
|
||||
// window as defence-in-depth (audit T6): even if the renderer were compromised
|
||||
// (e.g. XSS via remote video metadata) it can't open the camera/mic, read
|
||||
// location, or reach USB/HID/serial/Bluetooth devices — none of which the IPC
|
||||
// surface grants either. Clipboard (paste/copy) and everything else is left to
|
||||
// the default so the app's own features keep working.
|
||||
const DENIED_PERMISSIONS = new Set([
|
||||
'media', // camera + microphone
|
||||
'geolocation',
|
||||
'midi',
|
||||
'midiSysex',
|
||||
'hid',
|
||||
'serial',
|
||||
'usb',
|
||||
'bluetooth',
|
||||
'speaker-selection',
|
||||
'idle-detection'
|
||||
])
|
||||
|
||||
function createWindow(): void {
|
||||
const win = new BrowserWindow({
|
||||
width: 920,
|
||||
@@ -143,6 +162,15 @@ function createWindow(): void {
|
||||
// away from it (defence in depth; HMR uses websockets, not navigation).
|
||||
win.webContents.on('will-navigate', (e) => e.preventDefault())
|
||||
|
||||
// Deny the sensitive hardware/location web permissions the app never uses, so
|
||||
// a compromised renderer can't escalate to capabilities the IPC surface
|
||||
// doesn't grant. Both the async request and the sync check are covered. (audit T6)
|
||||
const ses = win.webContents.session
|
||||
ses.setPermissionRequestHandler((_wc, permission, callback) =>
|
||||
callback(!DENIED_PERMISSIONS.has(permission))
|
||||
)
|
||||
ses.setPermissionCheckHandler((_wc, permission) => !DENIED_PERMISSIONS.has(permission))
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
win.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user