mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(docker): harden docker-setup mount validation
This commit is contained in:
@@ -8,6 +8,11 @@ IMAGE_NAME="${OPENCLAW_IMAGE:-openclaw:local}"
|
|||||||
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
|
EXTRA_MOUNTS="${OPENCLAW_EXTRA_MOUNTS:-}"
|
||||||
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
|
HOME_VOLUME_NAME="${OPENCLAW_HOME_VOLUME:-}"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "ERROR: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
require_cmd() {
|
require_cmd() {
|
||||||
if ! command -v "$1" >/dev/null 2>&1; then
|
if ! command -v "$1" >/dev/null 2>&1; then
|
||||||
echo "Missing dependency: $1" >&2
|
echo "Missing dependency: $1" >&2
|
||||||
@@ -15,6 +20,44 @@ require_cmd() {
|
|||||||
fi
|
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
|
require_cmd docker
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
echo "Docker Compose not available (try: docker compose version)" >&2
|
echo "Docker Compose not available (try: docker compose version)" >&2
|
||||||
@@ -24,6 +67,19 @@ fi
|
|||||||
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}"
|
||||||
OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}"
|
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_CONFIG_DIR"
|
||||||
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
|
mkdir -p "$OPENCLAW_WORKSPACE_DIR"
|
||||||
|
|
||||||
@@ -57,6 +113,9 @@ write_extra_compose() {
|
|||||||
local home_volume="$1"
|
local home_volume="$1"
|
||||||
shift
|
shift
|
||||||
local mount
|
local mount
|
||||||
|
local gateway_home_mount
|
||||||
|
local gateway_config_mount
|
||||||
|
local gateway_workspace_mount
|
||||||
|
|
||||||
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
cat >"$EXTRA_COMPOSE_FILE" <<'YAML'
|
||||||
services:
|
services:
|
||||||
@@ -65,12 +124,19 @@ services:
|
|||||||
YAML
|
YAML
|
||||||
|
|
||||||
if [[ -n "$home_volume" ]]; then
|
if [[ -n "$home_volume" ]]; then
|
||||||
printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE"
|
gateway_home_mount="${home_volume}:/home/node"
|
||||||
printf ' - %s:/home/node/.openclaw\n' "$OPENCLAW_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE"
|
gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw"
|
||||||
printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
|
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
|
fi
|
||||||
|
|
||||||
for mount in "$@"; do
|
for mount in "$@"; do
|
||||||
|
validate_mount_spec "$mount"
|
||||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -80,16 +146,18 @@ YAML
|
|||||||
YAML
|
YAML
|
||||||
|
|
||||||
if [[ -n "$home_volume" ]]; then
|
if [[ -n "$home_volume" ]]; then
|
||||||
printf ' - %s:/home/node\n' "$home_volume" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s:/home/node/.openclaw\n' "$OPENCLAW_CONFIG_DIR" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
printf ' - %s:/home/node/.openclaw/workspace\n' "$OPENCLAW_WORKSPACE_DIR" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for mount in "$@"; do
|
for mount in "$@"; do
|
||||||
|
validate_mount_spec "$mount"
|
||||||
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
printf ' - %s\n' "$mount" >>"$EXTRA_COMPOSE_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
if [[ -n "$home_volume" && "$home_volume" != *"/"* ]]; then
|
||||||
|
validate_named_volume "$home_volume"
|
||||||
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
|
cat >>"$EXTRA_COMPOSE_FILE" <<YAML
|
||||||
volumes:
|
volumes:
|
||||||
${home_volume}:
|
${home_volume}:
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/ho
|
|||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- Paths must be shared with Docker Desktop on macOS/Windows.
|
- Paths must be shared with Docker Desktop on macOS/Windows.
|
||||||
|
- Each entry must be `source:target[:options]` with no spaces, tabs, or newlines.
|
||||||
- If you edit `OPENCLAW_EXTRA_MOUNTS`, rerun `docker-setup.sh` to regenerate the
|
- If you edit `OPENCLAW_EXTRA_MOUNTS`, rerun `docker-setup.sh` to regenerate the
|
||||||
extra compose file.
|
extra compose file.
|
||||||
- `docker-compose.extra.yml` is generated. Don’t hand-edit it.
|
- `docker-compose.extra.yml` is generated. Don’t hand-edit it.
|
||||||
@@ -158,6 +159,7 @@ export OPENCLAW_EXTRA_MOUNTS="$HOME/.codex:/home/node/.codex:ro,$HOME/github:/ho
|
|||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
- Named volumes must match `^[A-Za-z0-9][A-Za-z0-9_.-]*$`.
|
||||||
- If you change `OPENCLAW_HOME_VOLUME`, rerun `docker-setup.sh` to regenerate the
|
- If you change `OPENCLAW_HOME_VOLUME`, rerun `docker-setup.sh` to regenerate the
|
||||||
extra compose file.
|
extra compose file.
|
||||||
- The named volume persists until removed with `docker volume rm <name>`.
|
- The named volume persists until removed with `docker volume rm <name>`.
|
||||||
|
|||||||
@@ -137,6 +137,60 @@ describe("docker-setup.sh", () => {
|
|||||||
expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
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 () => {
|
it("avoids associative arrays so the script remains Bash 3.2-compatible", async () => {
|
||||||
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
const script = await readFile(join(repoRoot, "docker-setup.sh"), "utf8");
|
||||||
expect(script).not.toMatch(/^\s*declare -A\b/m);
|
expect(script).not.toMatch(/^\s*declare -A\b/m);
|
||||||
|
|||||||
Reference in New Issue
Block a user