Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
b739113
chore: add package-lock.json files for openclaw extensions
FletcherFrimpong Mar 26, 2026
3e4cdf7
security: fix XSS, CSP, command injection, and dependency vulnerabili…
FletcherFrimpong Mar 26, 2026
4bdb444
security: migrate JWT from globalState to SecretStorage (OS keychain)
FletcherFrimpong Mar 26, 2026
58a1669
security: verify openclaw package integrity before install
FletcherFrimpong Mar 26, 2026
f00e771
fix: fetch download URLs client-side to fix stale versions on static …
FletcherFrimpong Mar 26, 2026
2e41e5a
fix(ssh): verify install script integrity before execution
FletcherFrimpong Mar 27, 2026
29a14ef
docs: fix contributor images to show all contributors
FletcherFrimpong Mar 31, 2026
61d92fd
security: move host connection configs to SecretStorage and add adapt…
FletcherFrimpong Mar 31, 2026
31cce1b
security: verify integrity of all install paths, eliminate curl-pipe-…
FletcherFrimpong Mar 31, 2026
229a4dd
security: detect exposed gateway and harden health checks
FletcherFrimpong Mar 31, 2026
49d0470
security: implement SSH host key verification to prevent MITM attacks
FletcherFrimpong Mar 31, 2026
52592ff
security: harden version checks and download URLs against spoofing
FletcherFrimpong Mar 31, 2026
19e8530
security: harden Docker containers and audit for dangerous configurat…
FletcherFrimpong Mar 31, 2026
5a16020
security(P0): remove arbitrary command execution and all curl-pipe-bash
FletcherFrimpong Mar 31, 2026
b0207a7
security(P1): URL allowlist, API key via env var, filtered exec env
FletcherFrimpong Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ This project is open source. See the [LICENSE](LICENSE) file for details.

## Contributors

<a href=https://github.com/damoahdominic/occ/graphs/contributors>
<img src=https://contrib.rocks/image?repo=damoahdominic/occ />
<a href="https://github.com/damoahdominic/occ/graphs/contributors">
<img src="https://contrib.rocks/image?repo=damoahdominic/occ&anon=1" />
</a>

Made with [contrib.rocks](https://contrib.rocks).
Expand Down
59 changes: 59 additions & 0 deletions apps/editor/extensions/openclaw-docker/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions apps/editor/extensions/openclaw-docker/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,53 @@ export class DockerHostAdapter implements HostAdapter {
}

const containerId = await resolveContainerId(dockerConfig);

// Security audit: warn about dangerous container configurations
void this._auditContainerSecurity(containerId, dockerConfig);

return new DockerHostConnection(dockerConfig, containerId);
}

/**
* Audits a container for dangerous security configurations and warns the user.
* Checks: running as root, Docker socket mounted, privileged mode.
*/
private async _auditContainerSecurity(containerId: string, config: DockerConnection): Promise<void> {
try {
const hostArgs = config.dockerHost ? ['-H', config.dockerHost] : [];
const inspect = cp.spawnSync('docker', [
...hostArgs, 'inspect',
'--format', '{{.Config.User}}||{{.HostConfig.Privileged}}||{{range .Mounts}}{{.Source}}:{{end}}',
containerId,
], { timeout: 5000, windowsHide: true });
if (inspect.status !== 0) { return; }
const output = inspect.stdout.toString().trim();
const [user, privileged, mounts] = output.split('||');
const warnings: string[] = [];

// Check if running as root
if (!user || user === 'root' || user === '0') {
warnings.push('Container is running as root.');
}
// Check for privileged mode
if (privileged === 'true') {
warnings.push('Container is running in --privileged mode (near-full host access).');
}
// Check for Docker socket mount
if (mounts?.includes('/var/run/docker.sock')) {
warnings.push('Docker socket is mounted — container can control the Docker daemon.');
}

if (warnings.length > 0) {
void vscode.window.showWarningMessage(
`⚠ Container security: ${warnings.join(' ')} ` +
`This increases the risk of container escape. Consider using a non-root user and dropping capabilities.`,
'Dismiss',
);
}
} catch { /* best-effort audit */ }
}

async testConnection(config: HostConnectionConfig): Promise<TestResult> {
const dockerConfig = config as DockerConnection;

Expand Down
42 changes: 30 additions & 12 deletions apps/editor/extensions/openclaw-docker/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import * as vscode from 'vscode';
import * as cp from 'child_process';
import * as os from 'os';
import * as path from 'path';

/** Build a filtered environment — only safe variables, no leaked credentials. */
function safeExecEnv(): Record<string, string | undefined> {
const safe = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE',
'TERM', 'TMPDIR', 'XDG_RUNTIME_DIR', 'DISPLAY', 'WAYLAND_DISPLAY',
'NODE_ENV', 'NVM_DIR', 'NVM_BIN', 'DOCKER_HOST'];
const env: Record<string, string | undefined> = {};
for (const key of safe) {
if (process.env[key]) { env[key] = process.env[key]; }
}
return env;
}
import type {
HostConnection,
HostType,
Expand Down Expand Up @@ -211,15 +223,21 @@ export class DockerHostConnection implements HostConnection {

async installCli(onLog: LogFn): Promise<void> {
onLog('Installing OpenClaw inside container...\n');
const code = await this.execStream(
'bash',
['-c', 'curl -fsSL https://get.openclaw.sh | bash'],
{ timeout: 120_000 },
onLog,
onLog,
);
if (code !== 0) {
throw new Error(`Installer exited with code ${code}`);
// Download script, verify checksum, then execute — never pipe curl directly to bash.
const steps = [
{ label: 'Downloading installer...\n', cmd: 'curl -fsSL https://get.openclaw.sh -o /tmp/occ-install.sh' },
{ label: 'Fetching checksum...\n', cmd: 'curl -fsSL https://releases.openclaw.sh/install.sh.sha256 -o /tmp/occ-install.sha256' },
{ label: 'Verifying installer integrity...\n', cmd: 'cd /tmp && sha256sum -c occ-install.sha256' },
{ label: 'Running installer...\n', cmd: 'bash /tmp/occ-install.sh' },
{ label: '', cmd: 'rm -f /tmp/occ-install.sh /tmp/occ-install.sha256' },
];
for (const step of steps) {
if (step.label) { onLog(step.label); }
const code = await this.execStream('bash', ['-c', step.cmd], { timeout: 120_000 }, onLog, onLog);
if (code !== 0 && step.label) {
await this.exec('rm', ['-f', '/tmp/occ-install.sh', '/tmp/occ-install.sha256']).catch(() => {});
throw new Error(`Install step failed: ${step.label.trim()}`);
}
}
onLog('OpenClaw installed in container.\n');
}
Expand Down Expand Up @@ -315,18 +333,18 @@ export class DockerHostConnection implements HostConnection {
async runSetup(params: SetupParams, onLog: LogFn): Promise<void> {
const cliPath = await this.findOpenClawPath();
if (!cliPath) { throw new Error('OpenClaw CLI not installed — call installCli first'); }
// Pass API key via environment variable — never as a CLI argument (visible in ps/docker logs).
const args = ['onboard',
'--provider', params.provider,
'--api-key', params.apiKey,
'--port', params.port,
];
const code = await this.execStream(cliPath, args, {}, onLog, onLog);
const code = await this.execStream(cliPath, args, { env: { OPENCLAW_API_KEY: params.apiKey } }, onLog, onLog);
if (code !== 0) { throw new Error(`onboard exited with code ${code}`); }
}

// ── Environment ───────────────────────────

buildExecEnv(): Record<string, string | undefined> {
return { ...process.env };
return safeExecEnv();
}
}
2 changes: 1 addition & 1 deletion apps/editor/extensions/openclaw-docker/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
}

const adapter = new DockerHostAdapter();
const disposable = coreAPI.registerHostAdapter(adapter);
const disposable = coreAPI.registerHostAdapter(adapter, 'openclaw.openclaw-docker');
context.subscriptions.push(disposable);

const setupCmd = vscode.commands.registerCommand('openclaw.host.setup.docker', () => {
Expand Down
10 changes: 8 additions & 2 deletions apps/editor/extensions/openclaw-docker/src/setup-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,15 @@ export class DockerSetupPanel {
};

try {
logCmd(`$ docker run --rm \\\n -v ${VOLUME_MOUNT} \\\n ${IMAGE} \\\n openclaw onboard --non-interactive --accept-risk \\\n --flow quickstart --auth-choice custom-api-key \\\n --custom-base-url https://occ.mba.sh/v1 \\\n --gateway-auth token --gateway-port ${CONTAINER_PORT}\n`);
logCmd(`$ docker run --rm \\\n -v ${VOLUME_MOUNT} \\\n --security-opt no-new-privileges \\\n --cap-drop ALL \\\n --cap-add NET_BIND_SERVICE \\\n ${IMAGE} \\\n openclaw onboard --non-interactive --accept-risk \\\n --flow quickstart --auth-choice custom-api-key \\\n --custom-base-url https://occ.mba.sh/v1 \\\n --gateway-auth token --gateway-port ${CONTAINER_PORT}\n`);
log('Running OpenClaw onboard in container...\n');
const code = await new Promise<number>((resolve) => {
const proc = cp.spawn('docker', [
'run', '--rm',
'-v', VOLUME_MOUNT,
'--security-opt', 'no-new-privileges',
'--cap-drop', 'ALL',
'--cap-add', 'NET_BIND_SERVICE',
IMAGE,
'openclaw', 'onboard',
'--non-interactive', '--accept-risk',
Expand Down Expand Up @@ -386,7 +389,7 @@ export class DockerSetupPanel {
cp.spawnSync('docker', ['rm', '-f', CONTAINER], { timeout: 10000, windowsHide: true });
}

logCmd(`$ docker run -d \\\n --name ${CONTAINER} \\\n --restart unless-stopped \\\n -p ${HOST_PORT}:${CONTAINER_PORT} \\\n -v ${VOLUME_MOUNT} \\\n ${IMAGE} \\\n tail -f /dev/null\n`);
logCmd(`$ docker run -d \\\n --name ${CONTAINER} \\\n --restart unless-stopped \\\n -p ${HOST_PORT}:${CONTAINER_PORT} \\\n -v ${VOLUME_MOUNT} \\\n --security-opt no-new-privileges \\\n --cap-drop ALL \\\n --cap-add NET_BIND_SERVICE \\\n ${IMAGE} \\\n tail -f /dev/null\n`);
log(`Starting ${CONTAINER} container (port ${HOST_PORT}:${CONTAINER_PORT})...\n`);
const launchCode = await new Promise<number>((resolve) => {
const proc = cp.spawn('docker', [
Expand All @@ -395,6 +398,9 @@ export class DockerSetupPanel {
'--restart', 'unless-stopped',
'-p', `${HOST_PORT}:${CONTAINER_PORT}`,
'-v', VOLUME_MOUNT,
'--security-opt', 'no-new-privileges',
'--cap-drop', 'ALL',
'--cap-add', 'NET_BIND_SERVICE',
IMAGE,
'tail', '-f', '/dev/null',
], { windowsHide: true });
Expand Down
59 changes: 59 additions & 0 deletions apps/editor/extensions/openclaw-local/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 30 additions & 14 deletions apps/editor/extensions/openclaw-local/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';

/** Build a filtered environment — only safe variables, no leaked credentials. */
function safeExecEnv(): Record<string, string | undefined> {
const safe = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE',
'TERM', 'TMPDIR', 'XDG_RUNTIME_DIR', 'DISPLAY', 'WAYLAND_DISPLAY',
'NODE_ENV', 'NVM_DIR', 'NVM_BIN', 'DOCKER_HOST'];
const env: Record<string, string | undefined> = {};
for (const key of safe) {
if (process.env[key]) { env[key] = process.env[key]; }
}
return env;
}
import type {
HostConnection,
HostType,
Expand Down Expand Up @@ -235,17 +247,21 @@ export class LocalHostConnection implements HostConnection {
}

private async _installUnix(onLog: LogFn): Promise<void> {
onLog('Downloading OpenClaw installer...\n');
// Use curl to pipe the official install script
const code = await this.execStream(
'bash',
['-c', 'curl -fsSL https://get.openclaw.sh | bash'],
{ timeout: 120_000 },
onLog,
onLog,
);
if (code !== 0) {
throw new Error(`Installer exited with code ${code}`);
// Download script, verify checksum, then execute — never pipe curl directly to bash.
const steps = [
{ label: 'Downloading installer...\n', cmd: 'curl -fsSL https://get.openclaw.sh -o /tmp/occ-install.sh' },
{ label: 'Fetching checksum...\n', cmd: 'curl -fsSL https://releases.openclaw.sh/install.sh.sha256 -o /tmp/occ-install.sha256' },
{ label: 'Verifying installer integrity...\n', cmd: 'cd /tmp && sha256sum -c occ-install.sha256' },
{ label: 'Running installer...\n', cmd: 'bash /tmp/occ-install.sh' },
{ label: '', cmd: 'rm -f /tmp/occ-install.sh /tmp/occ-install.sha256' },
];
for (const step of steps) {
if (step.label) { onLog(step.label); }
const code = await this.execStream('bash', ['-c', step.cmd], { timeout: 120_000 }, onLog, onLog);
if (code !== 0 && step.label) {
await this.exec('rm', ['-f', '/tmp/occ-install.sh', '/tmp/occ-install.sha256']).catch(() => {});
throw new Error(`Install step failed: ${step.label.trim()}`);
}
}
onLog('OpenClaw installed.\n');
}
Expand Down Expand Up @@ -354,19 +370,19 @@ export class LocalHostConnection implements HostConnection {

onLog(`Setting up OpenClaw with provider=${params.provider}, port=${params.port}\n`);

// Pass API key via environment variable — never as a CLI argument (visible in ps).
const args = ['onboard',
'--provider', params.provider,
'--api-key', params.apiKey,
'--port', params.port,
];

const code = await this.execStream(cliPath, args, {}, onLog, onLog);
const code = await this.execStream(cliPath, args, { env: { OPENCLAW_API_KEY: params.apiKey } }, onLog, onLog);
if (code !== 0) { throw new Error(`onboard exited with code ${code}`); }
}

// ── Environment ───────────────────────────

buildExecEnv(): Record<string, string | undefined> {
return { ...process.env };
return safeExecEnv();
}
}
2 changes: 1 addition & 1 deletion apps/editor/extensions/openclaw-local/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
}

const adapter = new LocalHostAdapter();
const disposable = coreAPI.registerHostAdapter(adapter);
const disposable = coreAPI.registerHostAdapter(adapter, 'openclaw.openclaw-local');
context.subscriptions.push(disposable);

const setupCmd = vscode.commands.registerCommand('openclaw.host.setup.local', () => {
Expand Down
Loading
Loading