diff --git a/docker-setup.sh b/docker-setup.sh index 1d2f5e53fd..00c3cf1924 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -8,6 +8,11 @@ IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}" EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}" HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}" +fail() { + echo "ERROR: $*" >&2 + exit 1 +} + require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "Missing dependency: $1" >&2 @@ -15,6 +20,44 @@ require_cmd() { fi } +contains_disallowed_chars() { + local value="$1" + [[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]] +} + +validate_mount_path_value() { + local label="$1" + local value="$2" + if [[ -z "$value" ]]; then + fail "$label cannot be empty." + fi + if contains_disallowed_chars "$value"; then + fail "$label contains unsupported control characters." + fi + if [[ "$value" =~ [[:space:]] ]]; then + fail "$label cannot contain whitespace." + fi +} + +validate_named_volume() { + local value="$1" + if [[ ! "$value" =~ ^[A-Za-z0-9][A-Za-z0-9_.-]*$ ]]; then + fail "OPENCLAW_HOME_VOLUME must match [A-Za-z0-9][A-Za-z0-9_.-]* when using a named volume." + fi +} + +validate_mount_spec() { + local mount="$1" + if contains_disallowed_chars "$mount"; then + fail "OPENCLAW_EXTRA_MOUNTS entries cannot contain control characters." + fi + # Keep mount specs strict to avoid YAML structure injection. + # Expected format: source:target[:options] + if [[ ! "$mount" =~ ^[^[:space:],:]+:[^[:space:],:]+(:[^[:space:],:]+)?$ ]]; then + fail "Invalid mount format '$mount'. Expected source:target[:options] without spaces." + fi +} + require_cmd docker if ! docker compose version >/dev/null 2>&1; then echo "Docker Compose not available (try: docker compose version)" >&2 @@ -24,6 +67,19 @@ fi OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" +validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR" +validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR" +if [[ -n "$HOME_VOLUME_NAME" ]]; then + if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then + validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME" + else + validate_named_volume "$HOME_VOLUME_NAME" + fi +fi +if contains_disallowed_chars "$EXTRA_MOUNTS"; then + fail "OPENCLAW_EXTRA_MOUNTS cannot contain control characters." +fi + mkdir -p "$OPENCLAW_CONFIG_DIR" mkdir -p "$OPENCLAW_WORKSPACE_DIR" @@ -57,6 +113,9 @@ write_extra_compose() { local home_volume="$1" shift local mount + local gateway_home_mount + local gateway_config_mount + local gateway_workspace_mount cat >"$EXTRA_COMPOSE_FILE" <<'YAML' services: @@ -65,12 +124,19 @@ services: YAML if [[ -n "$home_volume" ]]; then - printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw\n' "$OPENCLAW_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE" + gateway_home_mount="${home_volume}:/home/node" + gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw" + gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace" + validate_mount_spec "$gateway_home_mount" + validate_mount_spec "$gateway_config_mount" + validate_mount_spec "$gateway_workspace_mount" + printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" fi for mount in "$@"; do + validate_mount_spec "$mount" printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" done @@ -80,16 +146,18 @@ YAML YAML if [[ -n "$home_volume" ]]; then - printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw\n' "$OPENCLAW_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE" - printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" fi for mount in "$@"; do + validate_mount_spec "$mount" printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE" done if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then + validate_named_volume "$home_volume" cat >>"$EXTRA_COMPOSE_FILE" <`. diff --git a/src/docker-setup.test.ts b/src/docker-setup.test.ts index a0ff66350a..14dcd72b81 100644 --- a/src/docker-setup.test.ts +++ b/src/docker-setup.test.ts @@ -137,6 +137,60 @@ describe("docker-setup.sh", () => { expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential"); }); + it("rejects injected multiline OPENCLAW_EXTRA_MOUNTS values", async () => { + if (!sandbox) { + throw new Error("sandbox missing"); + } + + const result = spawnSync("bash", [sandbox.scriptPath], { + cwd: sandbox.rootDir, + env: createEnv(sandbox, { + OPENCLAW_EXTRA_MOUNTS: "/tmp:/tmp\n evil-service:\n image: alpine", + }), + encoding: "utf8", + stdio: ["ignore", "ignore", "pipe"], + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("OPENCLAW_EXTRA_MOUNTS cannot contain control characters"); + }); + + it("rejects invalid OPENCLAW_EXTRA_MOUNTS mount format", async () => { + if (!sandbox) { + throw new Error("sandbox missing"); + } + + const result = spawnSync("bash", [sandbox.scriptPath], { + cwd: sandbox.rootDir, + env: createEnv(sandbox, { + OPENCLAW_EXTRA_MOUNTS: "bad mount spec", + }), + encoding: "utf8", + stdio: ["ignore", "ignore", "pipe"], + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Invalid mount format"); + }); + + it("rejects invalid OPENCLAW_HOME_VOLUME names", async () => { + if (!sandbox) { + throw new Error("sandbox missing"); + } + + const result = spawnSync("bash", [sandbox.scriptPath], { + cwd: sandbox.rootDir, + env: createEnv(sandbox, { + OPENCLAW_HOME_VOLUME: "bad name", + }), + encoding: "utf8", + stdio: ["ignore", "ignore", "pipe"], + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("OPENCLAW_HOME_VOLUME must match"); + }); + it("avoids associative arrays so the script remains Bash 3.2-compatible", async () => { const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8"); expect(script).not.toMatch(/^\s*declare -A\b/m);