mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 12:17:40 +00:00
fix(sandbox): preserve array order in config hashing
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
102
src/agents/sandbox/config-hash.test.ts
Normal file
102
src/agents/sandbox/config-hash.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,24 +19,12 @@ type SandboxBrowserHashInput = {
|
|||||||
agentWorkspaceDir: string;
|
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 {
|
function normalizeForHash(value: unknown): unknown {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const normalized = value
|
return value.map(normalizeForHash).filter((item): item is unknown => item !== undefined);
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
if (value && typeof value === "object") {
|
if (value && typeof value === "object") {
|
||||||
const entries = Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b));
|
const entries = Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b));
|
||||||
@@ -52,22 +40,6 @@ function normalizeForHash(value: unknown): unknown {
|
|||||||
return value;
|
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 {
|
export function computeSandboxConfigHash(input: SandboxHashInput): string {
|
||||||
return computeHash(input);
|
return computeHash(input);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user