VYPR
Moderate severityNVD Advisory· Published Feb 19, 2026· Updated Feb 20, 2026

OpenClaw's sandbox config hash sorted primitive arrays and suppressed needed container recreation

CVE-2026-27007

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.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.152026.2.15

Affected products

1

Patches

1
41ded303b4f6

fix(sandbox): preserve array order in config hashing

https://github.com/openclaw/openclawPeter SteinbergerFeb 16, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.