From 3c31209ff1a94bb81844c882020cf138568864ff Mon Sep 17 00:00:00 2001 From: vybe Date: Wed, 20 May 2026 10:11:57 +0000 Subject: [PATCH] feat: add buildCopyCommand for safe clipboard command construction Uses validateClipboardCmd to reject untrusted input and properly escapes single quotes in text to prevent shell injection. --- tailscalectl/lib.js | 8 +++++++- test/lib.test.js | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/tailscalectl/lib.js b/tailscalectl/lib.js index bd45288..da35592 100644 --- a/tailscalectl/lib.js +++ b/tailscalectl/lib.js @@ -32,6 +32,12 @@ function validateClipboardCmd(cmd) { return typeof cmd === "string" && safeClipboardCmds.includes(cmd) } +function buildCopyCommand(text, clipboardCmd) { + if (!validateClipboardCmd(clipboardCmd)) return null + var escaped = text.replace(/'/g, "'\\''") + return ["sh", "-c", "printf '%s' '" + escaped + "' | " + clipboardCmd] +} + function errorMessage(cmd) { var messages = { "up": "Failed to connect to Tailscale", @@ -46,5 +52,5 @@ function errorMessage(cmd) { // CommonJS export for Node.js tests (ignored by QML) if (typeof module !== "undefined" && module.exports) { - module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd } + module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand } } diff --git a/test/lib.test.js b/test/lib.test.js index 1adbf28..598b1f5 100644 --- a/test/lib.test.js +++ b/test/lib.test.js @@ -1,7 +1,7 @@ import { test } from "node:test" import assert from "node:assert" import lib from "../tailscalectl/lib.js" -const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd } = lib +const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand } = lib test("parsePeers extracts exitNode from ExitNodeOption", () => { const peerMap = { @@ -96,3 +96,23 @@ test("validateClipboardCmd rejects empty and falsy inputs", () => { assert.strictEqual(validateClipboardCmd(null), false) assert.strictEqual(validateClipboardCmd(undefined), false) }) + +// --- buildCopyCommand --- + +test("buildCopyCommand returns safe command for whitelisted clipboard tool", () => { + const cmd = buildCopyCommand("hello", "wl-copy") + assert.ok(Array.isArray(cmd)) + assert.strictEqual(cmd[0], "sh") + assert.strictEqual(cmd[1], "-c") +}) + +test("buildCopyCommand returns null for invalid clipboard command", () => { + assert.strictEqual(buildCopyCommand("hello", "rm -rf /"), null) + assert.strictEqual(buildCopyCommand("hello", ""), null) + assert.strictEqual(buildCopyCommand("hello", null), null) +}) + +test("buildCopyCommand safely handles text with special characters", () => { + const cmd = buildCopyCommand("it's a 'test' with \"quotes\" and\nnewlines", "wl-copy") + assert.ok(Array.isArray(cmd)) +})