You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
record: selectDevices resolves the screen by a loose "Capture screen" substring, first-match-wins, so a crafted video device enumerating first shadows the real screen in screen.mp4 #70
selectDevices resolves the screen-capture index (for record -video) by scanning the parsed avfoundation video devices and taking the first whose name contains the substring "Capture screen":
The match is a loose, unanchored substring with first-occurrence-wins (break) and no ambiguity/duplicate handling. parseAVDevices captures the device name with the regex's trailing (.*) and only trims whitespace — no ^Capture screen \d+$ shape check. The resolved screenIndex is fed verbatim to ffmpeg's -i via screenArgs, so a video device whose OS-supplied name merely contains"Capture screen" and enumerates before the genuine screen device is captured into screen.mp4 in its place — silently, with no error and no roster surfaced.
The audio side of the same function is the hardened sibling: the microphone is captured via avfoundation ":default" (micArgs), never by a name match, precisely so a virtual audio driver cannot shadow the real device (the anti-shadowing roster documented at internal/record/record.go:475-478). The video path is the unswept sibling of that hardening.
Wrong implicit assumption
"The only avfoundation video device whose name contains Capture screen is the real OS screen-capture device, and it is the first such device in enumeration order." Both halves are false. On macOS, cameras (including virtual/USB cameras) and screen pseudo-devices share one namespace, device names are OS-supplied strings an attacker can influence (the exact boundary this repo already accepts for the audio side — see #68, "a crafted USB/virtual-audio device can set" device names), and avfoundation lists cameras before screen devices — so a crafted camera gets a lower index and matches first.
Exact trigger (input → observed vs correct)
Operator runs testimony record -video (screen capture is opt-in). An attacker-influenced avfoundation video device is present whose name contains Capture screen — e.g. a virtual camera named Capture screen 0, or a USB camera whose vendor string is Elgato Capture screen HD. The device listing (matching the repo's own fixture ordering) is:
[AVFoundation indev @ 0x…] AVFoundation video devices:
[AVFoundation indev @ 0x…] [0] Elgato Capture screen HD ← crafted/coincidental camera, lower index
[AVFoundation indev @ 0x…] [1] Capture screen 0 ← the genuine display
Observed:selectDevices matches index 0 first, breaks, and returns screenIndex = 0; screenArgs(0, out) emits -i 0; screen.mp4 records the crafted camera's video instead of the participant's screen. Exit 0, no warning — the operator believes the retained file is a faithful screen recording.
Correct: an anchored match (^Capture screen \d+$) rejects Elgato Capture screen HD, falls through to index 1, and captures the genuine display.
The repo's own test fixture confirms the decisive enumeration order: internal/record/record_test.go:92-97 lists Studio Display Camera at index [0] and the real Capture screen 0 at index [1] — a camera precedes the screen, so first-match-wins is exploitable rather than self-defeating.
Location
Defect: internal/record/recorders.go:130-135 (selectDevices, the loose strings.Contains(d.name, "Capture screen") + break).
Unanchored name capture: internal/record/recorders.go:78 (deviceLine regex, trailing (.*)) and :101.
Hardened sibling (audio): internal/record/recorders.go:43-52 (micArgs, avfoundation :default) and the anti-shadowing rationale at internal/record/record.go:475-478.
Scope caveat (for the maintainer)
screen.mp4 is retained evidence but is not yet consumed by any downstream pipeline command (internal/record/record.go:9), so the blast radius is bounded to the integrity of that one video artefact — hence severity:minor. The trigger requires -videoand an attacker-influenced (or coincidentally-named) video device on the operator's machine — the same crafted-device precondition the repo already accepted in-scope for the audio sibling (#68). On a stock, non-adversarial macOS roster no device collides with the substring, so the normal happy path is unaffected; this is an anti-shadowing/robustness gap, not a stock-input logic error.
CWE anchor
CWE-807 (Reliance on Untrusted Inputs in a Security Decision) / CWE-345 (Insufficient Verification of Data Authenticity); the loose-substring aspect is also CWE-20 (Improper Input Validation).
Sibling sweep (name-based device selection over avfoundation output)
Screen selection in selectDevices — this defect (the only place that selects a device by a name match over untrusted avfoundation output).
Microphone selection — NOT vulnerable: captured via avfoundation :default (micArgs), resolved by ffmpeg at capture time, never by a parsed name; the roster is only logged for visibility (formatAudioRoster, recorders.go:151). This is the deliberately-hardened sibling.
strconv.Atoi(m[1]) (recorders.go:97) — NOT a sibling defect: the parsed index is re-emitted via strconv.Itoa, never used as a slice subscript, and an out-of-range value makes Atoi error and the row is skipped.
Terminal sinks for device names (formatAudioRoster→SafeText, outputTail→SafeTextLines) — already hardened; not this class.
Correctness lens — CONFIRMED: "No anchoring, dedup, ambiguity warning, or downstream check exists in parseAVDevices/selectDevices/startRecorders/screenArgs; input {"Elgato Capture screen HD"@0, "Capture screen 0"@1} fixes screenIndex=0 and screen.mp4 records the camera, whereas an anchored ^Capture screen \d+$ would correctly select index 1 — a genuine divergence the code's own audio-side :default hardening shows the authors treat as a real threat."
Fix direction (one sentence, not prescriptive)
Match the screen device against the anchored canonical form (^Capture screen \d+$) rather than a loose substring — and consider surfacing a screen roster / refusing on ambiguity — so a non-screen device whose name merely contains the phrase cannot shadow the genuine display.
Summary
selectDevicesresolves the screen-capture index (forrecord -video) by scanning the parsed avfoundation video devices and taking the first whose name contains the substring"Capture screen":The match is a loose, unanchored substring with first-occurrence-wins (
break) and no ambiguity/duplicate handling.parseAVDevicescaptures the device name with the regex's trailing(.*)and only trims whitespace — no^Capture screen \d+$shape check. The resolvedscreenIndexis fed verbatim to ffmpeg's-iviascreenArgs, so a video device whose OS-supplied name merely contains"Capture screen"and enumerates before the genuine screen device is captured intoscreen.mp4in its place — silently, with no error and no roster surfaced.The audio side of the same function is the hardened sibling: the microphone is captured via avfoundation
":default"(micArgs), never by a name match, precisely so a virtual audio driver cannot shadow the real device (the anti-shadowing roster documented atinternal/record/record.go:475-478). The video path is the unswept sibling of that hardening.Wrong implicit assumption
"The only avfoundation video device whose name contains
Capture screenis the real OS screen-capture device, and it is the first such device in enumeration order." Both halves are false. On macOS, cameras (including virtual/USB cameras) and screen pseudo-devices share one namespace, device names are OS-supplied strings an attacker can influence (the exact boundary this repo already accepts for the audio side — see #68, "a crafted USB/virtual-audio device can set" device names), and avfoundation lists cameras before screen devices — so a crafted camera gets a lower index and matches first.Exact trigger (input → observed vs correct)
Operator runs
testimony record -video(screen capture is opt-in). An attacker-influenced avfoundation video device is present whose name containsCapture screen— e.g. a virtual camera namedCapture screen 0, or a USB camera whose vendor string isElgato Capture screen HD. The device listing (matching the repo's own fixture ordering) is:selectDevicesmatches index0first,breaks, and returnsscreenIndex = 0;screenArgs(0, out)emits-i 0;screen.mp4records the crafted camera's video instead of the participant's screen. Exit 0, no warning — the operator believes the retained file is a faithful screen recording.^Capture screen \d+$) rejectsElgato Capture screen HD, falls through to index1, and captures the genuine display.The repo's own test fixture confirms the decisive enumeration order:
internal/record/record_test.go:92-97listsStudio Display Cameraat index[0]and the realCapture screen 0at index[1]— a camera precedes the screen, so first-match-wins is exploitable rather than self-defeating.Location
internal/record/recorders.go:130-135(selectDevices, the loosestrings.Contains(d.name, "Capture screen")+break).internal/record/recorders.go:78(deviceLineregex, trailing(.*)) and:101.internal/record/record.go:471(probeDevices(ffmpeg, wantScreen)) →internal/record/record.go:495(screenArgs(screenIndex, out)) →internal/record/recorders.go:57-68(-i strconv.Itoa(screenIndex)) → retainedscreen.mp4(internal/record/record.go:9, "opt-in retained evidence").internal/record/recorders.go:43-52(micArgs, avfoundation:default) and the anti-shadowing rationale atinternal/record/record.go:475-478.Scope caveat (for the maintainer)
screen.mp4is retained evidence but is not yet consumed by any downstream pipeline command (internal/record/record.go:9), so the blast radius is bounded to the integrity of that one video artefact — hence severity:minor. The trigger requires-videoand an attacker-influenced (or coincidentally-named) video device on the operator's machine — the same crafted-device precondition the repo already accepted in-scope for the audio sibling (#68). On a stock, non-adversarial macOS roster no device collides with the substring, so the normal happy path is unaffected; this is an anti-shadowing/robustness gap, not a stock-input logic error.CWE anchor
CWE-807 (Reliance on Untrusted Inputs in a Security Decision) / CWE-345 (Insufficient Verification of Data Authenticity); the loose-substring aspect is also CWE-20 (Improper Input Validation).
Sibling sweep (name-based device selection over avfoundation output)
selectDevices— this defect (the only place that selects a device by a name match over untrusted avfoundation output).:default(micArgs), resolved by ffmpeg at capture time, never by a parsed name; the roster is only logged for visibility (formatAudioRoster,recorders.go:151). This is the deliberately-hardened sibling.strconv.Atoi(m[1])(recorders.go:97) — NOT a sibling defect: the parsed index is re-emitted viastrconv.Itoa, never used as a slice subscript, and an out-of-range value makesAtoierror and the row is skipped.formatAudioRoster→SafeText,outputTail→SafeTextLines) — already hardened; not this class.parseAVDevicessection-header confusion (record: parseAVDevices mistakes a device row for a section header when the device name contains "AVFoundation audio/video devices:", silently dropping and misclassifying inputs #68) — different mechanism (name-based selection, not section detection).Validator confirmations (both lenses, adversarial, independent)
-videois a real flag flowing toplan→probeDevices(wantScreen=true)→selectDevices; the parsed video name flows intoscreenIndexand verbatim into ffmpeg's-iviascreenArgs(record.go:495, recorders.go:62), andscreen.mp4is produced and retained. The repo's own fixture (record_test.go:94) puts the realCapture screen 0at index 1 behind a camera at index 0, so first-match-wins is exploitable; a virtual-camera name is the exact video analogue of the audio boundary record: parseAVDevices mistakes a device row for a section header when the device name contains "AVFoundation audio/video devices:", silently dropping and misclassifying inputs #68 already accepts."parseAVDevices/selectDevices/startRecorders/screenArgs; input{"Elgato Capture screen HD"@0, "Capture screen 0"@1}fixesscreenIndex=0andscreen.mp4records the camera, whereas an anchored^Capture screen \d+$would correctly select index 1 — a genuine divergence the code's own audio-side:defaulthardening shows the authors treat as a real threat."Fix direction (one sentence, not prescriptive)
Match the screen device against the anchored canonical form (
^Capture screen \d+$) rather than a loose substring — and consider surfacing a screen roster / refusing on ambiguity — so a non-screen device whose name merely contains the phrase cannot shadow the genuine display.