Shell interpolation of clipboard detection output in TailscaleWidget.qml #12
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The clipboard tool detection flow in
TailscaleWidget.qmlruns shell commands, captures raw stdout as a string, and later interpolates that string into another shell command for execution. This creates an unsafe data path where unvalidated shell output becomes executable code.Where
Detection — line 65:
The
detectClipboardProcess runs a compoundsh -ccommand that checks for clipboard tools viawhichand echoes the matching command string to stdout:The raw stdout is stored in
root.clipboardCmdon line 69:Execution — line 144:
The stored string is interpolated directly into a new
sh -ccommand:No validation is performed on
clipboardCmdbefore it's executed as shell code.Why It's a Problem
Shell code injection via
whichoutput. The detection command trusts the output ofwhichandechowithout validation. Ifwhichis aliased, wrapped, or otherwise compromised, its output could include arbitrary shell code that gets executed on line 144. For example, an alias likealias which='which; echo; malicious_command #'would append extra lines to the captured output.No whitelist validation. The code assumes the detection command will only produce one of four known strings (
dms cl copy,wl-copy,clipmanctl copy, ornone), but there's no enforcement of this. Any unexpected output — from a PATH manipulation, awhichwrapper, or even the logic bug in the&&/||chain — becomes executable shell code.Logic bug in detection command. The
&&/||chain on line 65 is malformed. Eachechois followed by||, meaning ifechoitself fails (unlikely but possible), the nextwhichruns. More importantly, the structure doesn't properly group the check-and-echo pairs, so the fallthrough behavior may not match intent.String interpolation instead of command arrays. The rest of the file uses safe command arrays (e.g.,
["tailscale", "status", "--json"]on line 81). The clipboard path is the only place that constructs a shell command string via concatenation, making it an outlier and a higher-risk surface.Cross-model audit consensus (5 models: qwen3.6-27b, claude-sonnet-4-6, openai/gpt-5.5, gemini-3.1-pro-preview, grok-4.3) flags this as the single most severe issue.
Key additional points from the group:
textis currently sound, the raw interpolation ofroot.clipboardCmd(populated fromwhichstdout) remains a latent injection surface for any future change to detection logic.sh -centirely for the copy operation. UseProcesswith stdin (ordms cl copy <text>as argv) instead of piping through shell.onExitedhandler oncopyProcess(success toast fires unconditionally at line 145-146), so failed clipboard commands are reported as successful to the user.This aligns closely with the existing description but adds the multi-model severity signal and the explicit "remove shell" direction.
Gemini also flagged a good point about the
whichcommand being non-POSIX compliant and not as universal as the POSIX commandcommand -v. While fixing this issue, serious consideration to switching to the POSIX compliant version should be given.See also #17 (premature success toast on copyProcess) — same clipboard copy path.
Proposed Path Forward — Clipboard Security Overhaul
Resolves: #12 (shell injection), #17 (premature Copied toast), #25 (no stderr capture)
Overview
These three issues share a common root: the clipboard code path is the only place in the widget that constructs shell commands via string interpolation, lacks exit-status verification, and discards stderr. The fix is a single cohesive refactor of the clipboard detection and execution flow.
Plan
Step 1 — Whitelist-based clipboard detection (fixes #12)
Replace the compound
sh -cdetection command with three separatewhichinvocations, each as a proper command array. Store only the binary path, not the full command string. IncopyToClipboard(), validateclipboardCmdagainst a hardcoded whitelist (["dms", "wl-copy", "clipmanctl"]) before constructing the copy command. If the value is not on the whitelist, fall through to the "No clipboard tool found" error.Construct the copy command as a proper array instead of string interpolation:
dms:["dms", "cl", "copy", text]— but this still needsprintf | pipe. Alternative: useprocess.stdinif Quickshell supports it, or keep thesh -cbut only interpolate the user-providedtext(which is already escaped) into aprintf, piped to a whitelisted binary name.The key change:
root.clipboardCmdstores only the binary name ("dms","wl-copy","clipmanctl", or""). The copy command is constructed from a switch/map that maps the binary name to its known-safe argument structure. No arbitrary string ever reachessh -c.Step 2 — Exit-status check on copyProcess (fixes #17)
Add an
onExitedhandler tocopyProcess. Move the "Copied" toast fromcopyToClipboard()intoonExited(code === 0). Add a failure toast forcode !== 0.Step 3 — Stderr capture on all Process components (fixes #25)
Add
stderr: StdioCollector {}totoggleProcess,exitNodeProcess, andcopyProcess. In eachonExitedhandler, ifcode !== 0, include the stderr text in the error toast (truncated to a reasonable length). This gives users and developers the actual reason a command failed.Success Criteria
clipboardCmdcan only contain one of four known-safe values after detection.copyProcess.onExitedfires before any toast is shown. Success toast only on exit code 0.Processcomponents capture stderr and include it in error toasts on failure.AFK Classification
Partial AFK. Steps 1-3 are pure code edits to
TailscaleWidget.qml. The agent can write the code and verify syntax. However, clipboard operations and toast display require a running DMS instance with a compositor — HITL needed to verify the popout UI, toast messages, and clipboard functionality actually work.Files Modified
tailscalectl/TailscaleWidget.qml— clipboard detection logic, copyProcess handler, stderr collectors on all processesAlso resolves: #17, #25
I'm not sold on this approach. It's better than what's there, but it feels overengineered and like we are papering over the problem.
The whitelist validation is only necessary because the design still trusts output from an external process (which). You're adding a guard to compensate for a design decision that doesn't need to exist in the first place. Strip out the detection phase entirely and the whitelist becomes unnecessary.
Here's what I'd rather see: just try dms cl copy first. This is a widget for Dank Material Shell. In the overwhelming majority of real-world deployments, dms is going to be present and control the clipboard. If dms exits non-zero (not found, failed, whatever), fall back to wl-copy. If that fails, try clipmanctl copy. Cache whichever one succeeds so we're not reprobing on every copy operation. This is functionally identical to the level of clipboard checking the whitelist method did. However, this approach:
I know sh -c still needs to be there for the stdin pipe (printf ... | dms cl copy), and that's fine — the key is that the binary name in that shell string comes from a hardcoded array in the source, never from subprocess output, and we remove overcomplicated checking that relies on other tools. That's the meaningful security line. It still isn't perfect, but it functionally achieves the same thing with a smaller surface, simpler code, and likely lower performance hit.
Proposed Path Forward — Clipboard Security Overhaul (Revised)
Resolves: #12 (shell injection), #17 (premature Copied toast), #25 (no stderr capture)
Overview
Per @jtmorris's feedback, the original plan was overengineered. The whitelist was compensating for a design flaw — trusting subprocess output. The correct fix is to eliminate the detection phase entirely and use a try-fallback-cached approach with hardcoded tool names. No subprocess output ever enters the command construction path.
Plan
Step 1 — Remove detectClipboard entirely (fixes #12 root cause)
Delete the
detectClipboardProcess component and all references to it:property string clipboardCmd: ""detectClipboard.onExitedhandlerstatusChecktrigger toComponent.onCompleted: statusCheck.running = trueso startup status polling still happensThis eliminates the entire injection surface. No
which, nocommand -v, no subprocess output stored in a property.Step 2 — Try-fallback-cached copy logic (fixes #12, #17)
Replace
copyToClipboard(text)with a state-machine approach:New properties:
copyToClipboard(text):_copyText = text_copyCurrentTooltocachedClipboardToolif non-empty, else"dms"(default first try)_executeCopy()_executeCopy():{"dms": "dms cl copy", "wl-copy": "wl-copy", "clipmanctl": "clipmanctl copy"}["sh", "-c", "printf '%s' '<escaped>' | <hardcodedCmd>"]— the binary name comes from source code, never from subprocess outputcopyProcesscopyProcess.onExited(code, status):code === 0: setcachedClipboardTool = _copyCurrentTool, show "Copied" toastcode !== 0: move to next tool in the hardcoded list["dms", "wl-copy", "clipmanctl"]. If more tools remain, update_copyCurrentTooland call_executeCopy()again. If all exhausted, show "No clipboard tool found" error toast.This means:
dms, falls back towl-copy, thenclipmanctlStep 3 — Stderr capture on all Process components (fixes #25)
Add
stderr: StdioCollector {}totoggleProcess,exitNodeProcess, andcopyProcess. In eachonExitedhandler, ifcode !== 0, include the stderr text in the error toast (truncated to ~120 chars). This gives users and developers the actual reason a command failed.For
copyProcess, stderr is only shown if all fallback tools fail (the final error toast).Success Criteria
detectClipboardProcess is removed. No startup detection phase exists.clipboardCmdproperty is removed. No subprocess output is stored or later interpolated.copyProcess.onExitedfires before any toast. Success toast only on exit code 0.Processcomponents capture stderr and include it in error toasts on failure.statusCheckfires at component load viaComponent.onCompleted.AFK Classification
Partial AFK. The code changes are well-defined QML edits. The agent can write the code and verify syntax/logic. However, clipboard operations, fallback behavior, and toast display require a running DMS instance — HITL needed to verify:
Files Modified
tailscalectl/TailscaleWidget.qml— remove detectClipboard, add try-fallback-cached copy logic, add stderr collectors, move initial statusCheck triggerLooks okay enough. Give it a shot @vybe.
@vibe's latest plan for clipboard security overhaul needs update in light of recent master commits (e.g.
93b257b,7c12793,a86d5e2,dddd921,ce8707c,b521af3) which:tailscalectl/lib.jstest/The plan's core "remove detectClipboard entirely + try-fallback-cached" approach was not taken. Instead, detection was retained + hardened with whitelist validation and safe command builders in lib.js.
This changes the implementation surface: future work must edit lib.js for clipboard logic (and add/update tests), not just QML. The onExited handling, stderr, etc. still apply but now integrate with lib functions.
Updated plan should target lib.js + TailscaleWidget.qml + tests, and verify via
npm testor equivalent.Written by AI agent working for @jtmorris. Model: Grok 4.3.
This has been fixed and merged to
vibesfor review. Here's what changed:Removed:
detectClipboardProcess and theclipboardCmdpropertywhichchain)parseClipboardDetectionfromlib.jsReplaced with:
["dms", "wl-copy", "clipmanctl"]validateClipboardTool()only accepts exact matches against the whitelist — no shell interpolationbuildCopyCommand()constructs commands from the whitelist map, not from user/detection inputAdditional improvements:
stderrStdioCollector to all Process components so error toasts include up to 120 chars of stderr detailThe unsafe data path (raw shell output → interpolated into another shell command) no longer exists. All 27 tests pass.
Ready for review on
vibes.