OpenClaw's sandbox config hash sorted primitive arrays and suppressed needed container recreation
Description
OpenClaw is a personal AI assistant. Prior to version 2026.2.15, normalizeForHash in src/agents/sandbox/config-hash.ts recursively sorted arrays that contained only primitive values. This made order-sensitive sandbox configuration arrays hash to the same value even when order changed. In OpenClaw sandbox flows, this hash is used to decide whether existing sandbox containers should be recreated. As a result, order-only config changes (for example Docker dns and binds array order) could be treated as unchanged and stale containers could be reused. This is a configuration integrity issue affecting sandbox recreation behavior. Starting in version 2026.2.15, array ordering is preserved during hash normalization; only object key ordering remains normalized for deterministic hashing.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.2.15 | 2026.2.15 |
Affected products
1Patches
141ded303b4f6fix(sandbox): preserve array order in config hashing
3 files changed · +104 −29
CHANGELOG.md+1 −0 modified@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh. +- Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh. - Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent. - Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code. - Discord: preserve channel session continuity when runtime payloads omit `message.channelId` by falling back to event/raw `channel_id` values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as `sessionKey=unknown`. (#17622) Thanks @shakkernerd.
src/agents/sandbox/config-hash.test.ts+102 −0 added@@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import type { SandboxDockerConfig } from "./types.js"; +import { computeSandboxBrowserConfigHash, computeSandboxConfigHash } from "./config-hash.js"; + +function createDockerConfig(overrides?: Partial<SandboxDockerConfig>): SandboxDockerConfig { + return { + image: "openclaw-sandbox:test", + containerPrefix: "openclaw-sbx-", + workdir: "/workspace", + readOnlyRoot: true, + tmpfs: ["/tmp", "/var/tmp", "/run"], + network: "none", + capDrop: ["ALL"], + env: { LANG: "C.UTF-8" }, + dns: ["1.1.1.1", "8.8.8.8"], + extraHosts: ["host.docker.internal:host-gateway"], + binds: ["/tmp/workspace:/workspace:rw", "/tmp/cache:/cache:ro"], + ...overrides, + }; +} + +describe("computeSandboxConfigHash", () => { + it("ignores object key order", () => { + const shared = { + workspaceAccess: "rw" as const, + workspaceDir: "/tmp/workspace", + agentWorkspaceDir: "/tmp/workspace", + }; + const left = computeSandboxConfigHash({ + ...shared, + docker: createDockerConfig({ + env: { + LANG: "C.UTF-8", + B: "2", + A: "1", + }, + }), + }); + const right = computeSandboxConfigHash({ + ...shared, + docker: createDockerConfig({ + env: { + A: "1", + B: "2", + LANG: "C.UTF-8", + }, + }), + }); + expect(left).toBe(right); + }); + + it("treats primitive array order as significant", () => { + const shared = { + workspaceAccess: "rw" as const, + workspaceDir: "/tmp/workspace", + agentWorkspaceDir: "/tmp/workspace", + }; + const left = computeSandboxConfigHash({ + ...shared, + docker: createDockerConfig({ + dns: ["1.1.1.1", "8.8.8.8"], + }), + }); + const right = computeSandboxConfigHash({ + ...shared, + docker: createDockerConfig({ + dns: ["8.8.8.8", "1.1.1.1"], + }), + }); + expect(left).not.toBe(right); + }); +}); + +describe("computeSandboxBrowserConfigHash", () => { + it("treats docker bind order as significant", () => { + const shared = { + browser: { + cdpPort: 9222, + vncPort: 5900, + noVncPort: 6080, + headless: false, + enableNoVnc: true, + }, + workspaceAccess: "rw" as const, + workspaceDir: "/tmp/workspace", + agentWorkspaceDir: "/tmp/workspace", + }; + const left = computeSandboxBrowserConfigHash({ + ...shared, + docker: createDockerConfig({ + binds: ["/tmp/workspace:/workspace:rw", "/tmp/cache:/cache:ro"], + }), + }); + const right = computeSandboxBrowserConfigHash({ + ...shared, + docker: createDockerConfig({ + binds: ["/tmp/cache:/cache:ro", "/tmp/workspace:/workspace:rw"], + }), + }); + expect(left).not.toBe(right); + }); +});
src/agents/sandbox/config-hash.ts+1 −29 modified@@ -19,24 +19,12 @@ type SandboxBrowserHashInput = { agentWorkspaceDir: string; }; -function isPrimitive(value: unknown): value is string | number | boolean | bigint | symbol | null { - return value === null || (typeof value !== "object" && typeof value !== "function"); -} function normalizeForHash(value: unknown): unknown { if (value === undefined) { return undefined; } if (Array.isArray(value)) { - const normalized = value - .map(normalizeForHash) - .filter((item): item is unknown => item !== undefined); - const primitives = normalized.filter(isPrimitive); - if (primitives.length === normalized.length) { - return [...primitives].toSorted((a, b) => - primitiveToString(a).localeCompare(primitiveToString(b)), - ); - } - return normalized; + return value.map(normalizeForHash).filter((item): item is unknown => item !== undefined); } if (value && typeof value === "object") { const entries = Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b)); @@ -52,22 +40,6 @@ function normalizeForHash(value: unknown): unknown { return value; } -function primitiveToString(value: unknown): string { - if (value === null) { - return "null"; - } - if (typeof value === "string") { - return value; - } - if (typeof value === "number") { - return String(value); - } - if (typeof value === "boolean") { - return value ? "true" : "false"; - } - return JSON.stringify(value); -} - export function computeSandboxConfigHash(input: SandboxHashInput): string { return computeHash(input); }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-xxvh-5hwj-42ppghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27007ghsaADVISORY
- github.com/openclaw/openclaw/commit/41ded303b4f6dae5afa854531ff837c3276ad60bghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/releases/tag/v2026.2.15ghsax_refsource_MISCWEB
- github.com/openclaw/openclaw/security/advisories/GHSA-xxvh-5hwj-42ppghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.