mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 02:41:24 +00:00
feat(sandbox): block container namespace joins by default
This commit is contained in:
@@ -181,6 +181,12 @@ describe("buildSandboxCreateArgs", () => {
|
||||
cfg: createSandboxConfig({ network: "host" }),
|
||||
expected: /network mode "host" is blocked/,
|
||||
},
|
||||
{
|
||||
name: "network container namespace join",
|
||||
containerName: "openclaw-sbx-container-network",
|
||||
cfg: createSandboxConfig({ network: "container:peer" }),
|
||||
expected: /network mode "container:peer" is blocked by default/,
|
||||
},
|
||||
{
|
||||
name: "seccomp unconfined",
|
||||
containerName: "openclaw-sbx-seccomp",
|
||||
@@ -271,4 +277,18 @@ describe("buildSandboxCreateArgs", () => {
|
||||
});
|
||||
expect(args).toEqual(expect.arrayContaining(["-v", "/tmp/override:/workspace:rw"]));
|
||||
});
|
||||
|
||||
it("allows container namespace join with explicit dangerous override", () => {
|
||||
const cfg = createSandboxConfig({
|
||||
network: "container:peer",
|
||||
dangerouslyAllowContainerNamespaceJoin: true,
|
||||
});
|
||||
const args = buildSandboxCreateArgs({
|
||||
name: "openclaw-sbx-container-network-override",
|
||||
cfg,
|
||||
scopeKey: "main",
|
||||
createdAtMs: 1700000000000,
|
||||
});
|
||||
expect(args).toEqual(expect.arrayContaining(["--network", "container:peer"]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import { readBrowserRegistry, updateBrowserRegistry } from "./registry.js";
|
||||
import { resolveSandboxAgentId, slugifySessionKey } from "./shared.js";
|
||||
import { isToolAllowed } from "./tool-policy.js";
|
||||
import type { SandboxBrowserContext, SandboxConfig } from "./types.js";
|
||||
import { validateNetworkMode } from "./validate-sandbox-security.js";
|
||||
|
||||
const HOT_BROWSER_WINDOW_MS = 5 * 60 * 1000;
|
||||
const CDP_SOURCE_RANGE_ENV_KEY = "OPENCLAW_BROWSER_CDP_SOURCE_RANGE";
|
||||
@@ -107,14 +108,15 @@ async function ensureSandboxBrowserImage(image: string) {
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureDockerNetwork(network: string) {
|
||||
async function ensureDockerNetwork(
|
||||
network: string,
|
||||
opts?: { allowContainerNamespaceJoin?: boolean },
|
||||
) {
|
||||
validateNetworkMode(network, {
|
||||
allowContainerNamespaceJoin: opts?.allowContainerNamespaceJoin === true,
|
||||
});
|
||||
const normalized = network.trim().toLowerCase();
|
||||
if (
|
||||
!normalized ||
|
||||
normalized === "bridge" ||
|
||||
normalized === "none" ||
|
||||
normalized.startsWith("container:")
|
||||
) {
|
||||
if (!normalized || normalized === "bridge" || normalized === "none") {
|
||||
return;
|
||||
}
|
||||
const inspect = await execDocker(["network", "inspect", network], { allowFailure: true });
|
||||
@@ -216,7 +218,9 @@ export async function ensureSandboxBrowser(params: {
|
||||
if (noVncEnabled) {
|
||||
noVncPassword = generateNoVncPassword();
|
||||
}
|
||||
await ensureDockerNetwork(browserDockerCfg.network);
|
||||
await ensureDockerNetwork(browserDockerCfg.network, {
|
||||
allowContainerNamespaceJoin: browserDockerCfg.dangerouslyAllowContainerNamespaceJoin === true,
|
||||
});
|
||||
await ensureSandboxBrowserImage(browserImage);
|
||||
const args = buildSandboxCreateArgs({
|
||||
name: containerName,
|
||||
|
||||
@@ -95,6 +95,15 @@ export function resolveSandboxDockerConfig(params: {
|
||||
dns: agentDocker?.dns ?? globalDocker?.dns,
|
||||
extraHosts: agentDocker?.extraHosts ?? globalDocker?.extraHosts,
|
||||
binds: binds.length ? binds : undefined,
|
||||
dangerouslyAllowReservedContainerTargets:
|
||||
agentDocker?.dangerouslyAllowReservedContainerTargets ??
|
||||
globalDocker?.dangerouslyAllowReservedContainerTargets,
|
||||
dangerouslyAllowExternalBindSources:
|
||||
agentDocker?.dangerouslyAllowExternalBindSources ??
|
||||
globalDocker?.dangerouslyAllowExternalBindSources,
|
||||
dangerouslyAllowContainerNamespaceJoin:
|
||||
agentDocker?.dangerouslyAllowContainerNamespaceJoin ??
|
||||
globalDocker?.dangerouslyAllowContainerNamespaceJoin,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -267,6 +267,7 @@ export function buildSandboxCreateArgs(params: {
|
||||
bindSourceRoots?: string[];
|
||||
allowSourcesOutsideAllowedRoots?: boolean;
|
||||
allowReservedContainerTargets?: boolean;
|
||||
allowContainerNamespaceJoin?: boolean;
|
||||
}) {
|
||||
// Runtime security validation: blocks dangerous bind mounts, network modes, and profiles.
|
||||
validateSandboxSecurity({
|
||||
@@ -278,6 +279,9 @@ export function buildSandboxCreateArgs(params: {
|
||||
allowReservedContainerTargets:
|
||||
params.allowReservedContainerTargets ??
|
||||
params.cfg.dangerouslyAllowReservedContainerTargets === true,
|
||||
dangerouslyAllowContainerNamespaceJoin:
|
||||
params.allowContainerNamespaceJoin ??
|
||||
params.cfg.dangerouslyAllowContainerNamespaceJoin === true,
|
||||
});
|
||||
|
||||
const createdAtMs = params.createdAtMs ?? Date.now();
|
||||
|
||||
@@ -222,6 +222,30 @@ describe("validateNetworkMode", () => {
|
||||
expect(() => validateNetworkMode(testCase.mode), testCase.mode).toThrow(testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("blocks container namespace joins by default", () => {
|
||||
const cases = [
|
||||
{
|
||||
mode: "container:abc123",
|
||||
expected: /network mode "container:abc123" is blocked by default/,
|
||||
},
|
||||
{
|
||||
mode: "CONTAINER:ABC123",
|
||||
expected: /network mode "CONTAINER:ABC123" is blocked by default/,
|
||||
},
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
expect(() => validateNetworkMode(testCase.mode), testCase.mode).toThrow(testCase.expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows container namespace joins with explicit dangerous override", () => {
|
||||
expect(() =>
|
||||
validateNetworkMode("container:abc123", {
|
||||
allowContainerNamespaceJoin: true,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateSeccompProfile", () => {
|
||||
|
||||
@@ -42,6 +42,10 @@ export type ValidateBindMountsOptions = {
|
||||
allowReservedContainerTargets?: boolean;
|
||||
};
|
||||
|
||||
export type ValidateNetworkModeOptions = {
|
||||
allowContainerNamespaceJoin?: boolean;
|
||||
};
|
||||
|
||||
export type BlockedBindReason =
|
||||
| { kind: "targets"; blockedPath: string }
|
||||
| { kind: "covers"; blockedPath: string }
|
||||
@@ -276,14 +280,30 @@ export function validateBindMounts(
|
||||
}
|
||||
}
|
||||
|
||||
export function validateNetworkMode(network: string | undefined): void {
|
||||
if (network && BLOCKED_NETWORK_MODES.has(network.trim().toLowerCase())) {
|
||||
export function validateNetworkMode(
|
||||
network: string | undefined,
|
||||
options?: ValidateNetworkModeOptions,
|
||||
): void {
|
||||
const normalized = network?.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLOCKED_NETWORK_MODES.has(normalized)) {
|
||||
throw new Error(
|
||||
`Sandbox security: network mode "${network}" is blocked. ` +
|
||||
'Network "host" mode bypasses container network isolation. ' +
|
||||
'Use "bridge" or "none" instead.',
|
||||
);
|
||||
}
|
||||
|
||||
if (normalized.startsWith("container:") && options?.allowContainerNamespaceJoin !== true) {
|
||||
throw new Error(
|
||||
`Sandbox security: network mode "${network}" is blocked by default. ` +
|
||||
'Network "container:*" joins another container namespace and bypasses sandbox network isolation. ' +
|
||||
"Use a custom bridge network, or set dangerouslyAllowContainerNamespaceJoin=true only when you fully trust this runtime.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSeccompProfile(profile: string | undefined): void {
|
||||
@@ -312,10 +332,13 @@ export function validateSandboxSecurity(
|
||||
network?: string;
|
||||
seccompProfile?: string;
|
||||
apparmorProfile?: string;
|
||||
dangerouslyAllowContainerNamespaceJoin?: boolean;
|
||||
} & ValidateBindMountsOptions,
|
||||
): void {
|
||||
validateBindMounts(cfg.binds, cfg);
|
||||
validateNetworkMode(cfg.network);
|
||||
validateNetworkMode(cfg.network, {
|
||||
allowContainerNamespaceJoin: cfg.dangerouslyAllowContainerNamespaceJoin === true,
|
||||
});
|
||||
validateSeccompProfile(cfg.seccompProfile);
|
||||
validateApparmorProfile(cfg.apparmorProfile);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user