fix(tmux): escape serverUrl in pane shell commands

This commit is contained in:
YeonGyu-Kim
2026-03-17 15:16:54 +09:00
parent 1070b9170f
commit 0471078006
14 changed files with 546 additions and 23 deletions
+4 -1
View File
@@ -3,6 +3,7 @@ import type { TmuxConfig } from "../../../config/schema"
import { getTmuxPath } from "../../../tools/interactive-bash/tmux-path-resolver"
import type { SpawnPaneResult } from "../types"
import { isInsideTmux } from "./environment"
import { shellEscapeForDoubleQuotedCommand } from "../../shell-env"
export async function replaceTmuxPane(
paneId: string,
@@ -35,7 +36,8 @@ export async function replaceTmuxPane(
await ctrlCProc.exited
const shell = process.env.SHELL || "/bin/sh"
const opencodeCmd = `${shell} -c 'opencode attach ${serverUrl} --session ${sessionId}'`
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
const proc = spawn([tmux, "respawn-pane", "-k", "-t", paneId, opencodeCmd], {
stdout: "pipe",
@@ -60,6 +62,7 @@ export async function replaceTmuxPane(
const titleStderr = await stderrPromise
log("[replaceTmuxPane] WARNING: failed to set pane title", {
paneId,
title,
exitCode: titleExitCode,
stderr: titleStderr.trim(),
})
@@ -0,0 +1,96 @@
import { describe, expect, it } from "bun:test"
import { shellEscapeForDoubleQuotedCommand } from "../../shell-env"
describe("given a serverUrl with shell metacharacters", () => {
describe("when building tmux spawn command with double quotes", () => {
it("then serverUrl is escaped to prevent shell injection", () => {
const serverUrl = "http://localhost:3000'; cat /etc/passwd; echo '"
const sessionId = "test-session"
const shell = "/bin/sh"
// Use double quotes for outer shell -c command, escape dangerous chars in URL
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
// The semicolon should be escaped so it's treated as literal, not separator
expect(opencodeCmd).toContain("\\;")
// The malicious content should be escaped - semicolons are now \\;
expect(opencodeCmd).not.toMatch(/[^\\];\s*cat/)
})
})
describe("when building tmux replace command", () => {
it("then serverUrl is escaped to prevent shell injection", () => {
const serverUrl = "http://localhost:3000'; rm -rf /; '"
const sessionId = "test-session"
const shell = "/bin/sh"
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
expect(opencodeCmd).toContain("\\;")
expect(opencodeCmd).not.toMatch(/[^\\];\s*rm/)
})
})
})
describe("given a normal serverUrl without shell metacharacters", () => {
describe("when building tmux spawn command", () => {
it("then serverUrl works correctly", () => {
const serverUrl = "http://localhost:3000"
const sessionId = "test-session"
const shell = "/bin/sh"
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
expect(opencodeCmd).toContain(serverUrl)
})
})
})
describe("given a serverUrl with dollar sign (command injection)", () => {
describe("when building tmux command", () => {
it("then dollar sign is escaped properly", () => {
const serverUrl = "http://localhost:3000$(whoami)"
const sessionId = "test-session"
const shell = "/bin/sh"
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
// The $ should be escaped to literal $
expect(opencodeCmd).toContain("\\$")
})
})
})
describe("given a serverUrl with backticks (command injection)", () => {
describe("when building tmux command", () => {
it("then backticks are escaped properly", () => {
const serverUrl = "http://localhost:3000`whoami`"
const sessionId = "test-session"
const shell = "/bin/sh"
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
expect(opencodeCmd).toContain("\\`")
})
})
})
describe("given a serverUrl with pipe operator", () => {
describe("when building tmux command", () => {
it("then pipe is escaped properly", () => {
const serverUrl = "http://localhost:3000 | ls"
const sessionId = "test-session"
const shell = "/bin/sh"
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
expect(opencodeCmd).toContain("\\|")
})
})
})
+3 -1
View File
@@ -5,6 +5,7 @@ import type { SpawnPaneResult } from "../types"
import type { SplitDirection } from "./environment"
import { isInsideTmux } from "./environment"
import { isServerRunning } from "./server-health"
import { shellEscapeForDoubleQuotedCommand } from "../../shell-env"
export async function spawnTmuxPane(
sessionId: string,
@@ -49,7 +50,8 @@ export async function spawnTmuxPane(
log("[spawnTmuxPane] all checks passed, spawning...")
const shell = process.env.SHELL || "/bin/sh"
const opencodeCmd = `${shell} -c 'opencode attach ${serverUrl} --session ${sessionId}'`
const escapedUrl = shellEscapeForDoubleQuotedCommand(serverUrl)
const opencodeCmd = `${shell} -c "opencode attach ${escapedUrl} --session ${sessionId}"`
const args = [
"split-window",