fix(tmux): escape serverUrl in pane shell commands
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export const PLUGIN_NAME = "oh-my-opencode"
|
||||
export const LEGACY_PLUGIN_NAME = "oh-my-openagent"
|
||||
export const CONFIG_BASENAME = "oh-my-opencode"
|
||||
export const LOG_FILENAME = "oh-my-opencode.log"
|
||||
export const CACHE_DIR_NAME = "oh-my-opencode"
|
||||
|
||||
@@ -109,3 +109,44 @@ export function buildEnvPrefix(
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a value for use in a double-quoted shell -c command argument.
|
||||
*
|
||||
* In shell -c "..." strings, these characters have special meaning and must be escaped:
|
||||
* - $ - variable expansion, command substitution $(...)
|
||||
* - ` - command substitution `...`
|
||||
* - \\ - escape character
|
||||
* - " - end quote
|
||||
* - ; | & - command separators
|
||||
* - # - comment
|
||||
* - () - grouping operators
|
||||
*
|
||||
* @param value - The value to escape
|
||||
* @returns Escaped value safe for double-quoted shell -c argument
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // For malicious input
|
||||
* const url = "http://localhost:3000'; cat /etc/passwd; echo '"
|
||||
* const escaped = shellEscapeForDoubleQuotedCommand(url)
|
||||
* // => "http://localhost:3000'\''; cat /etc/passwd; echo '"
|
||||
*
|
||||
* // Usage in command:
|
||||
* const cmd = `/bin/sh -c "opencode attach ${escaped} --session ${sessionId}"`
|
||||
* ```
|
||||
*/
|
||||
export function shellEscapeForDoubleQuotedCommand(value: string): string {
|
||||
// Order matters: escape backslash FIRST, then other characters
|
||||
return value
|
||||
.replace(/\\/g, "\\\\") // escape backslash first
|
||||
.replace(/\$/g, "\\$") // escape dollar sign
|
||||
.replace(/`/g, "\\`") // escape backticks
|
||||
.replace(/"/g, "\\\"") // escape double quotes
|
||||
.replace(/;/g, "\\;") // escape semicolon (command separator)
|
||||
.replace(/\|/g, "\\|") // escape pipe (command separator)
|
||||
.replace(/&/g, "\\&") // escape ampersand (command separator)
|
||||
.replace(/#/g, "\\#") // escape hash (comment)
|
||||
.replace(/\(/g, "\\(") // escape parentheses
|
||||
.replace(/\)/g, "\\)") // escape parentheses
|
||||
}
|
||||
|
||||
@@ -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("\\|")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user