mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 17:38:27 +00:00
fix(sandbox): require noVNC observer password auth
This commit is contained in:
16
src/agents/sandbox/browser.novnc-url.test.ts
Normal file
16
src/agents/sandbox/browser.novnc-url.test.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildNoVncObserverUrl } from "./browser.js";
|
||||
|
||||
describe("buildNoVncObserverUrl", () => {
|
||||
it("builds the default observer URL without password", () => {
|
||||
expect(buildNoVncObserverUrl(45678)).toBe(
|
||||
"http://127.0.0.1:45678/vnc.html?autoconnect=1&resize=remote",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds an encoded password query parameter when provided", () => {
|
||||
expect(buildNoVncObserverUrl(45678, "a+b c&d")).toBe(
|
||||
"http://127.0.0.1:45678/vnc.html?autoconnect=1&resize=remote&password=a%2Bb+c%26d",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
buildSandboxCreateArgs,
|
||||
dockerContainerState,
|
||||
execDocker,
|
||||
readDockerContainerEnvVar,
|
||||
readDockerContainerLabel,
|
||||
readDockerPort,
|
||||
} from "./docker.js";
|
||||
@@ -28,6 +29,23 @@ import { isToolAllowed } from "./tool-policy.js";
|
||||
import type { SandboxBrowserContext, SandboxConfig } from "./types.js";
|
||||
|
||||
const HOT_BROWSER_WINDOW_MS = 5 * 60 * 1000;
|
||||
const NOVNC_PASSWORD_ENV_KEY = "OPENCLAW_BROWSER_NOVNC_PASSWORD";
|
||||
|
||||
function generateNoVncPassword() {
|
||||
// VNC auth uses an 8-char password max.
|
||||
return crypto.randomBytes(4).toString("hex");
|
||||
}
|
||||
|
||||
export function buildNoVncObserverUrl(port: number, password?: string) {
|
||||
const query = new URLSearchParams({
|
||||
autoconnect: "1",
|
||||
resize: "remote",
|
||||
});
|
||||
if (password?.trim()) {
|
||||
query.set("password", password);
|
||||
}
|
||||
return `http://127.0.0.1:${port}/vnc.html?${query.toString()}`;
|
||||
}
|
||||
|
||||
async function waitForSandboxCdp(params: { cdpPort: number; timeoutMs: number }): Promise<boolean> {
|
||||
const deadline = Date.now() + Math.max(0, params.timeoutMs);
|
||||
@@ -140,8 +158,14 @@ export async function ensureSandboxBrowser(params: {
|
||||
let running = state.running;
|
||||
let currentHash: string | null = null;
|
||||
let hashMismatch = false;
|
||||
const noVncEnabled = params.cfg.browser.enableNoVnc && !params.cfg.browser.headless;
|
||||
let noVncPassword: string | undefined;
|
||||
|
||||
if (hasContainer) {
|
||||
if (noVncEnabled) {
|
||||
noVncPassword =
|
||||
(await readDockerContainerEnvVar(containerName, NOVNC_PASSWORD_ENV_KEY)) ?? undefined;
|
||||
}
|
||||
const registry = await readBrowserRegistry();
|
||||
const registryEntry = registry.entries.find((entry) => entry.containerName === containerName);
|
||||
currentHash = await readDockerContainerLabel(containerName, "openclaw.configHash");
|
||||
@@ -177,6 +201,9 @@ export async function ensureSandboxBrowser(params: {
|
||||
}
|
||||
|
||||
if (!hasContainer) {
|
||||
if (noVncEnabled) {
|
||||
noVncPassword = generateNoVncPassword();
|
||||
}
|
||||
await ensureSandboxBrowserImage(browserImage);
|
||||
const args = buildSandboxCreateArgs({
|
||||
name: containerName,
|
||||
@@ -201,7 +228,7 @@ export async function ensureSandboxBrowser(params: {
|
||||
);
|
||||
}
|
||||
args.push("-p", `127.0.0.1::${params.cfg.browser.cdpPort}`);
|
||||
if (params.cfg.browser.enableNoVnc && !params.cfg.browser.headless) {
|
||||
if (noVncEnabled) {
|
||||
args.push("-p", `127.0.0.1::${params.cfg.browser.noVncPort}`);
|
||||
}
|
||||
args.push("-e", `OPENCLAW_BROWSER_HEADLESS=${params.cfg.browser.headless ? "1" : "0"}`);
|
||||
@@ -209,6 +236,9 @@ export async function ensureSandboxBrowser(params: {
|
||||
args.push("-e", `OPENCLAW_BROWSER_CDP_PORT=${params.cfg.browser.cdpPort}`);
|
||||
args.push("-e", `OPENCLAW_BROWSER_VNC_PORT=${params.cfg.browser.vncPort}`);
|
||||
args.push("-e", `OPENCLAW_BROWSER_NOVNC_PORT=${params.cfg.browser.noVncPort}`);
|
||||
if (noVncEnabled && noVncPassword) {
|
||||
args.push("-e", `${NOVNC_PASSWORD_ENV_KEY}=${noVncPassword}`);
|
||||
}
|
||||
args.push(browserImage);
|
||||
await execDocker(args);
|
||||
await execDocker(["start", containerName]);
|
||||
@@ -221,10 +251,13 @@ export async function ensureSandboxBrowser(params: {
|
||||
throw new Error(`Failed to resolve CDP port mapping for ${containerName}.`);
|
||||
}
|
||||
|
||||
const mappedNoVnc =
|
||||
params.cfg.browser.enableNoVnc && !params.cfg.browser.headless
|
||||
? await readDockerPort(containerName, params.cfg.browser.noVncPort)
|
||||
: null;
|
||||
const mappedNoVnc = noVncEnabled
|
||||
? await readDockerPort(containerName, params.cfg.browser.noVncPort)
|
||||
: null;
|
||||
if (noVncEnabled && !noVncPassword) {
|
||||
noVncPassword =
|
||||
(await readDockerContainerEnvVar(containerName, NOVNC_PASSWORD_ENV_KEY)) ?? undefined;
|
||||
}
|
||||
|
||||
const existing = BROWSER_BRIDGES.get(params.scopeKey);
|
||||
const existingProfile = existing
|
||||
@@ -323,9 +356,7 @@ export async function ensureSandboxBrowser(params: {
|
||||
});
|
||||
|
||||
const noVncUrl =
|
||||
mappedNoVnc && params.cfg.browser.enableNoVnc && !params.cfg.browser.headless
|
||||
? `http://127.0.0.1:${mappedNoVnc}/vnc.html?autoconnect=1&resize=remote`
|
||||
: undefined;
|
||||
mappedNoVnc && noVncEnabled ? buildNoVncObserverUrl(mappedNoVnc, noVncPassword) : undefined;
|
||||
|
||||
return {
|
||||
bridgeUrl: resolvedBridge.baseUrl,
|
||||
|
||||
@@ -38,7 +38,7 @@ export const DEFAULT_TOOL_DENY = [
|
||||
|
||||
export const DEFAULT_SANDBOX_BROWSER_IMAGE = "openclaw-sandbox-browser:bookworm-slim";
|
||||
export const DEFAULT_SANDBOX_COMMON_IMAGE = "openclaw-sandbox-common:bookworm-slim";
|
||||
export const SANDBOX_BROWSER_SECURITY_HASH_EPOCH = "2026-02-21-no-sandbox-default";
|
||||
export const SANDBOX_BROWSER_SECURITY_HASH_EPOCH = "2026-02-21-novnc-auth-default";
|
||||
|
||||
export const DEFAULT_SANDBOX_BROWSER_PREFIX = "openclaw-sbx-browser-";
|
||||
export const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222;
|
||||
|
||||
@@ -145,6 +145,25 @@ export async function readDockerContainerLabel(
|
||||
return raw;
|
||||
}
|
||||
|
||||
export async function readDockerContainerEnvVar(
|
||||
containerName: string,
|
||||
envVar: string,
|
||||
): Promise<string | null> {
|
||||
const result = await execDocker(
|
||||
["inspect", "-f", "{{range .Config.Env}}{{println .}}{{end}}", containerName],
|
||||
{ allowFailure: true },
|
||||
);
|
||||
if (result.code !== 0) {
|
||||
return null;
|
||||
}
|
||||
for (const line of result.stdout.split(/\r?\n/)) {
|
||||
if (line.startsWith(`${envVar}=`)) {
|
||||
return line.slice(envVar.length + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function readDockerPort(containerName: string, port: number) {
|
||||
const result = await execDocker(["port", containerName, `${port}/tcp`], {
|
||||
allowFailure: true,
|
||||
|
||||
Reference in New Issue
Block a user