VYPR
Medium severity6.1NVD Advisory· Published Mar 18, 2026· Updated Apr 8, 2026

CVE-2026-22177

CVE-2026-22177

Description

OpenClaw versions prior to 2026.2.21 fail to filter dangerous process-control environment variables from config env.vars, allowing startup-time code execution. Attackers can inject variables like NODE_OPTIONS or LD_* through configuration to execute arbitrary code in the OpenClaw gateway service runtime context.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.212026.2.21

Affected products

1
  • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*
    Range: <2026.2.21

Patches

1
2cdbadee1f8f

fix(security): block startup-file env injection across host execution paths

https://github.com/openclaw/openclawPeter SteinbergerFeb 21, 2026via ghsa
13 files changed · +318 147
  • apps/macos/Sources/OpenClaw/ExecApprovalsSocket.swift+2 27 modified
    @@ -364,21 +364,6 @@ private enum ExecHostExecutor {
             let skillAllow: Bool
         }
     
    -    private static let blockedEnvKeys: Set<String> = [
    -        "PATH",
    -        "NODE_OPTIONS",
    -        "PYTHONHOME",
    -        "PYTHONPATH",
    -        "PERL5LIB",
    -        "PERL5OPT",
    -        "RUBYOPT",
    -    ]
    -
    -    private static let blockedEnvPrefixes: [String] = [
    -        "DYLD_",
    -        "LD_",
    -    ]
    -
         static func handle(_ request: ExecHostRequest) async -> ExecHostResponse {
             let command = request.command.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
             guard !command.isEmpty else {
    @@ -580,18 +565,8 @@ private enum ExecHostExecutor {
                 error: nil)
         }
     
    -    private static func sanitizedEnv(_ overrides: [String: String]?) -> [String: String]? {
    -        guard let overrides else { return nil }
    -        var merged = ProcessInfo.processInfo.environment
    -        for (rawKey, value) in overrides {
    -            let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
    -            guard !key.isEmpty else { continue }
    -            let upper = key.uppercased()
    -            if self.blockedEnvKeys.contains(upper) { continue }
    -            if self.blockedEnvPrefixes.contains(where: { upper.hasPrefix($0) }) { continue }
    -            merged[key] = value
    -        }
    -        return merged
    +    private static func sanitizedEnv(_ overrides: [String: String]?) -> [String: String] {
    +        HostEnvSanitizer.sanitize(overrides: overrides)
         }
     }
     
    
  • apps/macos/Sources/OpenClaw/HostEnvSanitizer.swift+54 0 added
    @@ -0,0 +1,54 @@
    +import Foundation
    +
    +enum HostEnvSanitizer {
    +    private static let blockedKeys: Set<String> = [
    +        "NODE_OPTIONS",
    +        "NODE_PATH",
    +        "PYTHONHOME",
    +        "PYTHONPATH",
    +        "PERL5LIB",
    +        "PERL5OPT",
    +        "RUBYLIB",
    +        "RUBYOPT",
    +        "BASH_ENV",
    +        "ENV",
    +        "GCONV_PATH",
    +        "IFS",
    +        "SSLKEYLOGFILE",
    +    ]
    +
    +    private static let blockedPrefixes: [String] = [
    +        "DYLD_",
    +        "LD_",
    +        "BASH_FUNC_",
    +    ]
    +
    +    private static func isBlocked(_ upperKey: String) -> Bool {
    +        if self.blockedKeys.contains(upperKey) { return true }
    +        return self.blockedPrefixes.contains(where: { upperKey.hasPrefix($0) })
    +    }
    +
    +    static func sanitize(overrides: [String: String]?) -> [String: String] {
    +        var merged: [String: String] = [:]
    +        for (rawKey, value) in ProcessInfo.processInfo.environment {
    +            let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
    +            guard !key.isEmpty else { continue }
    +            let upper = key.uppercased()
    +            if self.isBlocked(upper) { continue }
    +            merged[key] = value
    +        }
    +
    +        guard let overrides else { return merged }
    +        for (rawKey, value) in overrides {
    +            let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
    +            guard !key.isEmpty else { continue }
    +            let upper = key.uppercased()
    +            // PATH is part of the security boundary (command resolution + safe-bin checks). Never
    +            // allow request-scoped PATH overrides from agents/gateways.
    +            if upper == "PATH" { continue }
    +            if self.isBlocked(upper) { continue }
    +            merged[key] = value
    +        }
    +        return merged
    +    }
    +}
    
  • apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift+2 27 modified
    @@ -862,33 +862,8 @@ extension MacNodeRuntime {
             UserDefaults.standard.object(forKey: cameraEnabledKey) as? Bool ?? false
         }
     
    -    private static let blockedEnvKeys: Set<String> = [
    -        "PATH",
    -        "NODE_OPTIONS",
    -        "PYTHONHOME",
    -        "PYTHONPATH",
    -        "PERL5LIB",
    -        "PERL5OPT",
    -        "RUBYOPT",
    -    ]
    -
    -    private static let blockedEnvPrefixes: [String] = [
    -        "DYLD_",
    -        "LD_",
    -    ]
    -
    -    private static func sanitizedEnv(_ overrides: [String: String]?) -> [String: String]? {
    -        guard let overrides else { return nil }
    -        var merged = ProcessInfo.processInfo.environment
    -        for (rawKey, value) in overrides {
    -            let key = rawKey.trimmingCharacters(in: .whitespacesAndNewlines)
    -            guard !key.isEmpty else { continue }
    -            let upper = key.uppercased()
    -            if self.blockedEnvKeys.contains(upper) { continue }
    -            if self.blockedEnvPrefixes.contains(where: { upper.hasPrefix($0) }) { continue }
    -            merged[key] = value
    -        }
    -        return merged
    +    private static func sanitizedEnv(_ overrides: [String: String]?) -> [String: String] {
    +        HostEnvSanitizer.sanitize(overrides: overrides)
         }
     
         private nonisolated static func locationMode() -> OpenClawLocationMode {
    
  • CHANGELOG.md+1 0 modified
    @@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
     - Docker/Security: run E2E and install-sh test images as non-root by adding appuser directives. Thanks @thewilloftheshadow.
     - Skills/Security: sanitize skill env overrides to block unsafe runtime injection variables and only allow sensitive keys when declared in skill metadata, with warnings for suspicious values. Thanks @thewilloftheshadow.
     - Security/Browser: block non-network browser navigation protocols (including `file:`, `data:`, and `javascript:`) while preserving `about:blank`, preventing local file reads via browser tool navigation. This ships in the next npm release. Thanks @q1uf3ng for reporting.
    +- Security/Exec: block shell startup-file env injection (`BASH_ENV`, `ENV`, `BASH_FUNC_*`, `LD_*`, `DYLD_*`) across config env ingestion, node-host inherited environment sanitization, and macOS exec host runtime to prevent pre-command execution from attacker-controlled environment variables. Thanks @tdjackey.
     - Security/Gateway/Hooks: block `__proto__`, `constructor`, and `prototype` traversal in webhook template path resolution to prevent prototype-chain payload data leakage in `messageTemplate` rendering. (#22213) Thanks @SleuthCo.
     - Security/OpenClawKit/UI: prevent injected inbound user context metadata blocks from leaking into chat history in TUI, webchat, and macOS surfaces by stripping all untrusted metadata prefixes at display boundaries. (#22142) Thanks @Mellowambience, @vincentkoc.
     - Security/OpenClawKit/UI: strip inbound metadata blocks from user messages in TUI rendering while preserving user-authored content. (#22345) Thanks @kansodata, @vincentkoc.
    
  • src/agents/bash-tools.exec-runtime.ts+2 28 modified
    @@ -3,6 +3,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
     import { Type } from "@sinclair/typebox";
     import type { ExecAsk, ExecHost, ExecSecurity } from "../infra/exec-approvals.js";
     import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
    +import { isDangerousHostEnvVarName } from "../infra/host-env-security.js";
     import { mergePathPrepend } from "../infra/path-prepend.js";
     import { enqueueSystemEvent } from "../infra/system-events.js";
     import type { ProcessSession } from "./bash-process-registry.js";
    @@ -28,41 +29,14 @@ import {
     import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js";
     import { getShellConfig, sanitizeBinaryOutput } from "./shell-utils.js";
     
    -// Security: Blocklist of environment variables that could alter execution flow
    -// or inject code when running on non-sandboxed hosts (Gateway/Node).
    -const DANGEROUS_HOST_ENV_VARS = new Set([
    -  "LD_PRELOAD",
    -  "LD_LIBRARY_PATH",
    -  "LD_AUDIT",
    -  "DYLD_INSERT_LIBRARIES",
    -  "DYLD_LIBRARY_PATH",
    -  "NODE_OPTIONS",
    -  "NODE_PATH",
    -  "PYTHONPATH",
    -  "PYTHONHOME",
    -  "RUBYLIB",
    -  "PERL5LIB",
    -  "BASH_ENV",
    -  "ENV",
    -  "GCONV_PATH",
    -  "IFS",
    -  "SSLKEYLOGFILE",
    -]);
    -const DANGEROUS_HOST_ENV_PREFIXES = ["DYLD_", "LD_"];
    -
     // Centralized sanitization helper.
     // Throws an error if dangerous variables or PATH modifications are detected on the host.
     export function validateHostEnv(env: Record<string, string>): void {
       for (const key of Object.keys(env)) {
         const upperKey = key.toUpperCase();
     
         // 1. Block known dangerous variables (Fail Closed)
    -    if (DANGEROUS_HOST_ENV_PREFIXES.some((prefix) => upperKey.startsWith(prefix))) {
    -      throw new Error(
    -        `Security Violation: Environment variable '${key}' is forbidden during host execution.`,
    -      );
    -    }
    -    if (DANGEROUS_HOST_ENV_VARS.has(upperKey)) {
    +    if (isDangerousHostEnvVarName(upperKey)) {
           throw new Error(
             `Security Violation: Environment variable '${key}' is forbidden during host execution.`,
           );
    
  • src/agents/skills.e2e.test.ts+44 0 modified
    @@ -351,6 +351,50 @@ describe("applySkillEnvOverrides", () => {
         }
       });
     
    +  it("blocks dangerous host env overrides even when declared", async () => {
    +    const workspaceDir = await makeWorkspace();
    +    const skillDir = path.join(workspaceDir, "skills", "dangerous-env-skill");
    +    await writeSkill({
    +      dir: skillDir,
    +      name: "dangerous-env-skill",
    +      description: "Needs env",
    +      metadata: '{"openclaw":{"requires":{"env":["BASH_ENV"]}}}',
    +    });
    +
    +    const entries = loadWorkspaceSkillEntries(workspaceDir, {
    +      managedSkillsDir: path.join(workspaceDir, ".managed"),
    +    });
    +
    +    const originalBashEnv = process.env.BASH_ENV;
    +    delete process.env.BASH_ENV;
    +
    +    const restore = applySkillEnvOverrides({
    +      skills: entries,
    +      config: {
    +        skills: {
    +          entries: {
    +            "dangerous-env-skill": {
    +              env: {
    +                BASH_ENV: "/tmp/pwn.sh",
    +              },
    +            },
    +          },
    +        },
    +      },
    +    });
    +
    +    try {
    +      expect(process.env.BASH_ENV).toBeUndefined();
    +    } finally {
    +      restore();
    +      if (originalBashEnv === undefined) {
    +        expect(process.env.BASH_ENV).toBeUndefined();
    +      } else {
    +        expect(process.env.BASH_ENV).toBe(originalBashEnv);
    +      }
    +    }
    +  });
    +
       it("allows required env overrides from snapshots", async () => {
         const workspaceDir = await makeWorkspace();
         const skillDir = path.join(workspaceDir, "skills", "snapshot-env-skill");
    
  • src/agents/skills/env-overrides.ts+24 19 modified
    @@ -1,4 +1,5 @@
     import type { OpenClawConfig } from "../../config/config.js";
    +import { isDangerousHostEnvVarName } from "../../infra/host-env-security.js";
     import { sanitizeEnvVars, validateEnvVarValue } from "../sandbox/sanitize-env-vars.js";
     import { resolveSkillConfig } from "./config.js";
     import { resolveSkillKey } from "./frontmatter.js";
    @@ -13,18 +14,19 @@ type SanitizedSkillEnvOverrides = {
       warnings: string[];
     };
     
    -// Never allow skill env overrides that can alter runtime loader flags.
    -const HARD_BLOCKED_SKILL_ENV_PATTERNS: ReadonlyArray<RegExp> = [
    -  /^NODE_OPTIONS$/i,
    -  /^OPENSSL_CONF$/i,
    -  /^LD_PRELOAD$/i,
    -  /^DYLD_INSERT_LIBRARIES$/i,
    -];
    +// Always block skill env overrides that can alter runtime loading or host execution behavior.
    +const SKILL_ALWAYS_BLOCKED_ENV_PATTERNS: ReadonlyArray<RegExp> = [/^OPENSSL_CONF$/i];
     
     function matchesAnyPattern(value: string, patterns: readonly RegExp[]): boolean {
       return patterns.some((pattern) => pattern.test(value));
     }
     
    +function isAlwaysBlockedSkillEnvKey(key: string): boolean {
    +  return (
    +    isDangerousHostEnvVarName(key) || matchesAnyPattern(key, SKILL_ALWAYS_BLOCKED_ENV_PATTERNS)
    +  );
    +}
    +
     function sanitizeSkillEnvOverrides(params: {
       overrides: Record<string, string>;
       allowedSensitiveKeys: Set<string>;
    @@ -33,19 +35,22 @@ function sanitizeSkillEnvOverrides(params: {
         return { allowed: {}, blocked: [], warnings: [] };
       }
     
    -  const result = sanitizeEnvVars(params.overrides, {
    -    customBlockedPatterns: HARD_BLOCKED_SKILL_ENV_PATTERNS,
    -  });
    -  const allowed = { ...result.allowed };
    -  const blocked: string[] = [];
    +  const result = sanitizeEnvVars(params.overrides);
    +  const allowed: Record<string, string> = {};
    +  const blocked = new Set<string>();
       const warnings = [...result.warnings];
     
    +  for (const [key, value] of Object.entries(result.allowed)) {
    +    if (isAlwaysBlockedSkillEnvKey(key)) {
    +      blocked.add(key);
    +      continue;
    +    }
    +    allowed[key] = value;
    +  }
    +
       for (const key of result.blocked) {
    -    if (
    -      matchesAnyPattern(key, HARD_BLOCKED_SKILL_ENV_PATTERNS) ||
    -      !params.allowedSensitiveKeys.has(key)
    -    ) {
    -      blocked.push(key);
    +    if (isAlwaysBlockedSkillEnvKey(key) || !params.allowedSensitiveKeys.has(key)) {
    +      blocked.add(key);
           continue;
         }
         const value = params.overrides[key];
    @@ -55,15 +60,15 @@ function sanitizeSkillEnvOverrides(params: {
         const warning = validateEnvVarValue(value);
         if (warning) {
           if (warning === "Contains null bytes") {
    -        blocked.push(key);
    +        blocked.add(key);
             continue;
           }
           warnings.push(`${key}: ${warning}`);
         }
         allowed[key] = value;
       }
     
    -  return { allowed, blocked, warnings };
    +  return { allowed, blocked: [...blocked], warnings };
     }
     
     function applySkillConfigEnvOverrides(params: {
    
  • src/config/config.env-vars.test.ts+16 1 modified
    @@ -3,7 +3,7 @@ import path from "node:path";
     import { describe, expect, it } from "vitest";
     import { loadDotEnv } from "../infra/dotenv.js";
     import { resolveConfigEnvVars } from "./env-substitution.js";
    -import { applyConfigEnvVars } from "./env-vars.js";
    +import { applyConfigEnvVars, collectConfigEnvVars } from "./env-vars.js";
     import { withEnvOverride, withTempHome } from "./test-helpers.js";
     import type { OpenClawConfig } from "./types.js";
     
    @@ -29,6 +29,21 @@ describe("config env vars", () => {
         });
       });
     
    +  it("blocks dangerous startup env vars from config env", async () => {
    +    await withEnvOverride({ BASH_ENV: undefined, OPENROUTER_API_KEY: undefined }, async () => {
    +      const config = {
    +        env: { vars: { BASH_ENV: "/tmp/pwn.sh", OPENROUTER_API_KEY: "config-key" } },
    +      };
    +      const entries = collectConfigEnvVars(config as OpenClawConfig);
    +      expect(entries.BASH_ENV).toBeUndefined();
    +      expect(entries.OPENROUTER_API_KEY).toBe("config-key");
    +
    +      applyConfigEnvVars(config as OpenClawConfig);
    +      expect(process.env.BASH_ENV).toBeUndefined();
    +      expect(process.env.OPENROUTER_API_KEY).toBe("config-key");
    +    });
    +  });
    +
       it("loads ${VAR} substitutions from ~/.openclaw/.env on repeated runtime loads", async () => {
         await withTempHome(async (_home) => {
           await withEnvOverride({ BRAVE_API_KEY: undefined }, async () => {
    
  • src/config/env-vars.ts+7 0 modified
    @@ -1,3 +1,4 @@
    +import { isDangerousHostEnvVarName } from "../infra/host-env-security.js";
     import type { OpenClawConfig } from "./types.js";
     
     export function collectConfigEnvVars(cfg?: OpenClawConfig): Record<string, string> {
    @@ -13,6 +14,9 @@ export function collectConfigEnvVars(cfg?: OpenClawConfig): Record<string, strin
           if (!value) {
             continue;
           }
    +      if (isDangerousHostEnvVarName(key)) {
    +        continue;
    +      }
           entries[key] = value;
         }
       }
    @@ -24,6 +28,9 @@ export function collectConfigEnvVars(cfg?: OpenClawConfig): Record<string, strin
         if (typeof value !== "string" || !value.trim()) {
           continue;
         }
    +    if (isDangerousHostEnvVarName(key)) {
    +      continue;
    +    }
         entries[key] = value;
       }
     
    
  • src/infra/host-env-security.test.ts+51 0 added
    @@ -0,0 +1,51 @@
    +import { describe, expect, it } from "vitest";
    +import { isDangerousHostEnvVarName, sanitizeHostExecEnv } from "./host-env-security.js";
    +
    +describe("isDangerousHostEnvVarName", () => {
    +  it("matches dangerous keys and prefixes case-insensitively", () => {
    +    expect(isDangerousHostEnvVarName("BASH_ENV")).toBe(true);
    +    expect(isDangerousHostEnvVarName("bash_env")).toBe(true);
    +    expect(isDangerousHostEnvVarName("DYLD_INSERT_LIBRARIES")).toBe(true);
    +    expect(isDangerousHostEnvVarName("ld_preload")).toBe(true);
    +    expect(isDangerousHostEnvVarName("BASH_FUNC_echo%%")).toBe(true);
    +    expect(isDangerousHostEnvVarName("PATH")).toBe(false);
    +    expect(isDangerousHostEnvVarName("FOO")).toBe(false);
    +  });
    +});
    +
    +describe("sanitizeHostExecEnv", () => {
    +  it("removes dangerous inherited keys while preserving PATH", () => {
    +    const env = sanitizeHostExecEnv({
    +      baseEnv: {
    +        PATH: "/usr/bin:/bin",
    +        BASH_ENV: "/tmp/pwn.sh",
    +        LD_PRELOAD: "/tmp/pwn.so",
    +        OK: "1",
    +      },
    +    });
    +
    +    expect(env).toEqual({
    +      PATH: "/usr/bin:/bin",
    +      OK: "1",
    +    });
    +  });
    +
    +  it("blocks PATH and dangerous override values", () => {
    +    const env = sanitizeHostExecEnv({
    +      baseEnv: {
    +        PATH: "/usr/bin:/bin",
    +        HOME: "/tmp/home",
    +      },
    +      overrides: {
    +        PATH: "/tmp/evil",
    +        BASH_ENV: "/tmp/pwn.sh",
    +        SAFE: "ok",
    +      },
    +    });
    +
    +    expect(env.PATH).toBe("/usr/bin:/bin");
    +    expect(env.BASH_ENV).toBeUndefined();
    +    expect(env.SAFE).toBe("ok");
    +    expect(env.HOME).toBe("/tmp/home");
    +  });
    +});
    
  • src/infra/host-env-security.ts+74 0 added
    @@ -0,0 +1,74 @@
    +const HOST_DANGEROUS_ENV_KEY_VALUES = [
    +  "NODE_OPTIONS",
    +  "NODE_PATH",
    +  "PYTHONHOME",
    +  "PYTHONPATH",
    +  "PERL5LIB",
    +  "PERL5OPT",
    +  "RUBYLIB",
    +  "RUBYOPT",
    +  "BASH_ENV",
    +  "ENV",
    +  "GCONV_PATH",
    +  "IFS",
    +  "SSLKEYLOGFILE",
    +] as const;
    +
    +export const HOST_DANGEROUS_ENV_KEYS = new Set<string>(HOST_DANGEROUS_ENV_KEY_VALUES);
    +export const HOST_DANGEROUS_ENV_PREFIXES = ["DYLD_", "LD_", "BASH_FUNC_"] as const;
    +
    +export function isDangerousHostEnvVarName(key: string): boolean {
    +  const upper = key.toUpperCase();
    +  if (HOST_DANGEROUS_ENV_KEYS.has(upper)) {
    +    return true;
    +  }
    +  return HOST_DANGEROUS_ENV_PREFIXES.some((prefix) => upper.startsWith(prefix));
    +}
    +
    +export function sanitizeHostExecEnv(params?: {
    +  baseEnv?: Record<string, string | undefined>;
    +  overrides?: Record<string, string> | null;
    +  blockPathOverrides?: boolean;
    +}): Record<string, string> {
    +  const baseEnv = params?.baseEnv ?? process.env;
    +  const overrides = params?.overrides ?? undefined;
    +  const blockPathOverrides = params?.blockPathOverrides ?? true;
    +
    +  const merged: Record<string, string> = {};
    +  for (const [rawKey, value] of Object.entries(baseEnv)) {
    +    if (typeof value !== "string") {
    +      continue;
    +    }
    +    const key = rawKey.trim();
    +    if (!key || isDangerousHostEnvVarName(key)) {
    +      continue;
    +    }
    +    merged[key] = value;
    +  }
    +
    +  if (!overrides) {
    +    return merged;
    +  }
    +
    +  for (const [rawKey, value] of Object.entries(overrides)) {
    +    if (typeof value !== "string") {
    +      continue;
    +    }
    +    const key = rawKey.trim();
    +    if (!key) {
    +      continue;
    +    }
    +    const upper = key.toUpperCase();
    +    // PATH is part of the security boundary (command resolution + safe-bin checks). Never allow
    +    // request-scoped PATH overrides from agents/gateways.
    +    if (blockPathOverrides && upper === "PATH") {
    +      continue;
    +    }
    +    if (isDangerousHostEnvVarName(upper)) {
    +      continue;
    +    }
    +    merged[key] = value;
    +  }
    +
    +  return merged;
    +}
    
  • src/node-host/invoke.sanitize-env.test.ts+38 7 modified
    @@ -7,7 +7,7 @@ describe("node-host sanitizeEnv", () => {
         const prev = process.env.PATH;
         process.env.PATH = "/usr/bin";
         try {
    -      const env = sanitizeEnv({ PATH: "/tmp/evil:/usr/bin" }) ?? {};
    +      const env = sanitizeEnv({ PATH: "/tmp/evil:/usr/bin" });
           expect(env.PATH).toBe("/usr/bin");
         } finally {
           if (prev === undefined) {
    @@ -21,18 +21,21 @@ describe("node-host sanitizeEnv", () => {
       it("blocks dangerous env keys/prefixes", () => {
         const prevPythonPath = process.env.PYTHONPATH;
         const prevLdPreload = process.env.LD_PRELOAD;
    +    const prevBashEnv = process.env.BASH_ENV;
         try {
           delete process.env.PYTHONPATH;
           delete process.env.LD_PRELOAD;
    -      const env =
    -        sanitizeEnv({
    -          PYTHONPATH: "/tmp/pwn",
    -          LD_PRELOAD: "/tmp/pwn.so",
    -          FOO: "bar",
    -        }) ?? {};
    +      delete process.env.BASH_ENV;
    +      const env = sanitizeEnv({
    +        PYTHONPATH: "/tmp/pwn",
    +        LD_PRELOAD: "/tmp/pwn.so",
    +        BASH_ENV: "/tmp/pwn.sh",
    +        FOO: "bar",
    +      });
           expect(env.FOO).toBe("bar");
           expect(env.PYTHONPATH).toBeUndefined();
           expect(env.LD_PRELOAD).toBeUndefined();
    +      expect(env.BASH_ENV).toBeUndefined();
         } finally {
           if (prevPythonPath === undefined) {
             delete process.env.PYTHONPATH;
    @@ -44,6 +47,34 @@ describe("node-host sanitizeEnv", () => {
           } else {
             process.env.LD_PRELOAD = prevLdPreload;
           }
    +      if (prevBashEnv === undefined) {
    +        delete process.env.BASH_ENV;
    +      } else {
    +        process.env.BASH_ENV = prevBashEnv;
    +      }
    +    }
    +  });
    +
    +  it("drops dangerous inherited env keys even without overrides", () => {
    +    const prevPath = process.env.PATH;
    +    const prevBashEnv = process.env.BASH_ENV;
    +    try {
    +      process.env.PATH = "/usr/bin:/bin";
    +      process.env.BASH_ENV = "/tmp/pwn.sh";
    +      const env = sanitizeEnv(undefined);
    +      expect(env.PATH).toBe("/usr/bin:/bin");
    +      expect(env.BASH_ENV).toBeUndefined();
    +    } finally {
    +      if (prevPath === undefined) {
    +        delete process.env.PATH;
    +      } else {
    +        process.env.PATH = prevPath;
    +      }
    +      if (prevBashEnv === undefined) {
    +        delete process.env.BASH_ENV;
    +      } else {
    +        process.env.BASH_ENV = prevBashEnv;
    +      }
         }
       });
     });
    
  • src/node-host/invoke.ts+3 38 modified
    @@ -32,6 +32,7 @@ import {
       type ExecHostRunResult,
     } from "../infra/exec-host.js";
     import { getTrustedSafeBinDirs } from "../infra/exec-safe-bin-trust.js";
    +import { sanitizeHostExecEnv } from "../infra/host-env-security.js";
     import { validateSystemRunCommandConsistency } from "../infra/system-run-command.js";
     import { runBrowserProxyCommand } from "./invoke-browser.js";
     
    @@ -43,17 +44,6 @@ const execHostEnforced = process.env.OPENCLAW_NODE_EXEC_HOST?.trim().toLowerCase
     const execHostFallbackAllowed =
       process.env.OPENCLAW_NODE_EXEC_FALLBACK?.trim().toLowerCase() !== "0";
     
    -const blockedEnvKeys = new Set([
    -  "NODE_OPTIONS",
    -  "PYTHONHOME",
    -  "PYTHONPATH",
    -  "PERL5LIB",
    -  "PERL5OPT",
    -  "RUBYOPT",
    -]);
    -
    -const blockedEnvPrefixes = ["DYLD_", "LD_"];
    -
     type SystemRunParams = {
       command: string[];
       rawCommand?: string | null;
    @@ -136,33 +126,8 @@ function resolveExecAsk(value?: string): ExecAsk {
       return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss";
     }
     
    -export function sanitizeEnv(
    -  overrides?: Record<string, string> | null,
    -): Record<string, string> | undefined {
    -  if (!overrides) {
    -    return undefined;
    -  }
    -  const merged = { ...process.env } as Record<string, string>;
    -  for (const [rawKey, value] of Object.entries(overrides)) {
    -    const key = rawKey.trim();
    -    if (!key) {
    -      continue;
    -    }
    -    const upper = key.toUpperCase();
    -    // PATH is part of the security boundary (command resolution + safe-bin checks). Never allow
    -    // request-scoped PATH overrides from agents/gateways.
    -    if (upper === "PATH") {
    -      continue;
    -    }
    -    if (blockedEnvKeys.has(upper)) {
    -      continue;
    -    }
    -    if (blockedEnvPrefixes.some((prefix) => upper.startsWith(prefix))) {
    -      continue;
    -    }
    -    merged[key] = value;
    -  }
    -  return merged;
    +export function sanitizeEnv(overrides?: Record<string, string> | null): Record<string, string> {
    +  return sanitizeHostExecEnv({ overrides, blockPathOverrides: true });
     }
     
     function truncateOutput(raw: string, maxChars: number): { text: string; truncated: boolean } {
    

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

6

News mentions

0

No linked articles in our index yet.