Skip to content

Exec: harden host env override handling across gateway and node#51207

Merged
joshavant merged 4 commits into
mainfrom
feat/host-env-override-hardening-phase1
Mar 20, 2026
Merged

Exec: harden host env override handling across gateway and node#51207
joshavant merged 4 commits into
mainfrom
feat/host-env-override-hardening-phase1

Conversation

@joshavant

Copy link
Copy Markdown
Contributor

Summary

  • tighten host env override handling for host exec paths
  • align gateway and node behavior behind one host env sanitization flow
  • fail closed on blocked or invalid override keys before execution begins
  • expand override blocklist coverage for additional runtime loader and config keys
  • regenerate the macOS host env policy artifact

Validation

  • pnpm test -- src/infra/host-env-security.test.ts src/agents/bash-tools.exec.path.test.ts src/node-host/invoke-system-run.test.ts
  • pnpm build
  • pnpm check (currently fails in unrelated pre-existing file: docs/plugins/sdk-migration.md formatting)
  • OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test (currently fails in unrelated pre-existing extension deps: fake-indexeddb/auto in extensions/matrix)

@aisle-research-bot

aisle-research-bot Bot commented Mar 20, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Log/control-character injection via reflected invalid env override keys in system.run denial message (Node)
2 🔵 Low Log/control-character injection via reflected invalid env override keys in system.run denial message (macOS Swift)

1. 🔵 Log/control-character injection via reflected invalid env override keys in system.run denial message (Node)

Property Value
Severity Low
CWE CWE-117
Location src/node-host/invoke-system-run.ts:250-275

Description

system.run now rejects blocked/invalid environment override keys and reflects the rejected key names back in the error message. Because rejected invalid keys are derived from untrusted JSON object keys and are not escaped/neutralized, an attacker can include control characters (e.g. newlines, tabs, ANSI escapes) in a key name and have them appear in downstream logs/UI.

Key points:

  • Input: opts.params.env keys come from an external request.
  • Taint propagation: invalid keys are collected as rawKey.trim() (preserves embedded control characters).
  • Sink: the collected keys are interpolated into error.message and returned to the caller; this message is then propagated by the gateway (and may be logged by server/client components), enabling log forging / multiline injection.

Vulnerable code (sink):

message: `SYSTEM_RUN_DENIED: environment override rejected (${details.join("; ")})`

where details includes joined rejected keys.

Recommendation

Avoid reflecting untrusted key material verbatim in error messages/logs.

Options:

  1. Escape/neutralize control characters before joining:
function escapeForDiagnostic(s: string): string {// Replace control chars (including newlines/escapes) with visible escapes
  return s.replace(/[\u0000-\u001F\u007F]/g, (ch) => {
    const code = ch.codePointAt(0)!.toString(16).padStart(2, "0");
    return `\\x${code}`;
  });
}

const safeInvalid = envOverrideDiagnostics.rejectedOverrideInvalidKeys.map(escapeForDiagnostic);
  1. Or, do not include the actual keys; include only counts and a generic hint.

Apply the same escaping anywhere rejected keys are included in diagnostics (source lists are built in inspectHostExecEnvOverrides).


2. 🔵 Log/control-character injection via reflected invalid env override keys in system.run denial message (macOS Swift)

Property Value
Severity Low
CWE CWE-117
Location apps/macos/Sources/OpenClaw/NodeMode/MacNodeRuntime.swift:468-484

Description

The macOS node runtime rejects blocked/invalid environment override keys and reflects the rejected key names inside an error message returned to callers. The invalid key strings are derived from untrusted JSON dictionary keys (params.env) and are not escaped/neutralized before being embedded into the message.

Impact:

  • An attacker can send an env override key containing control characters (e.g. "BAD\nKEY" or ANSI escapes).
  • The key is collected into invalidKeys (using trimmingCharacters(in: .whitespacesAndNewlines) which does not remove embedded control characters).
  • The key is then joined and interpolated into the denial message, which may be logged or displayed, enabling log forging/multiline injection or terminal escape injection depending on the consumer.

Vulnerable code:

invalid.append(candidate.isEmpty ? rawKey : candidate)
...
message: "SYSTEM_RUN_DENIED: environment override rejected (\(details.joined(separator: "; ")) )"

where details includes joined invalidKeys.

Recommendation

Do not embed untrusted strings verbatim into diagnostic messages.

Recommended fix:

  • Escape control characters before joining/embedding, or replace the list with a count.

Example escaping helper:

func escapeForDiagnostic(_ s: String) -> String {
    var out = ""
    for scalar in s.unicodeScalars {
        if scalar.value < 0x20 || scalar.value == 0x7F {
            out += String(format: "\\x%02X", scalar.value)
        } else {
            out.unicodeScalars.append(scalar)
        }
    }
    return out
}

let safeInvalid = envOverrideDiagnostics.invalidKeys.map(escapeForDiagnostic)

Then use safeInvalid.joined(separator: ", ").

Also apply similar escaping in HostEnvSanitizer.inspectOverrides when collecting invalid keys, if those are used anywhere else.


Analyzed PR: #51207 at commit fb2b239

Last updated on: 2026-03-20T21:55:49Z

@openclaw-barnacle openclaw-barnacle Bot added app: macos App: macos agents Agent runtime and tooling size: M maintainer Maintainer-authored PR labels Mar 20, 2026
@greptile-apps

greptile-apps Bot commented Mar 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens host environment variable override handling across the gateway (bash-tools.exec.ts) and node (invoke-system-run.ts) execution paths by introducing a unified diagnostic sanitization API (sanitizeHostExecEnvWithDiagnostics) that surfaces rejected keys, then uses those diagnostics to fail closed with explicit error messages before execution begins. It also expands the override blocklist with 16 new runtime-loader and config keys (Java CLASSPATH, Go CGO/GOFLAGS, .NET CORECLR_PROFILER_PATH, PHP, Deno, Bun, Lua, Ruby, Composer, XDG_CONFIG_HOME) and regenerates the macOS companion Swift policy artifact.

Key changes and observations:

  • Gateway path (bash-tools.exec.ts): Replaces the old sanitizeHostBaseEnv + validateHostEnv split with sanitizeHostExecEnvWithDiagnostics. The resulting hostEnvResult.env is correctly used as the actual execution env — clean and consistent.
  • Node-host path (invoke-system-run.ts): sanitizeHostExecEnvWithDiagnostics is called without baseEnv, defaulting to process.env, but envSanitizeResult.env is never used — the actual execution env is still produced by opts.sanitizeEnv(envOverrides). This causes unnecessary computation (building a full merged env from process.env + accepted overrides only to discard it) and introduces a subtle dual-path inconsistency where the diagnostic and execution envs use different base env references.
  • Shell-wrapper case: When shellPayload !== null, sanitizeSystemRunEnvOverrides filters opts.params.env down to only the shell-wrapper-allowed keys before the new check runs. A blocked key like CLASSPATH is silently dropped, never reaching the new rejection logic. The "fail closed with an explicit error" guarantee only applies to the direct-exec (non-shell-wrapper) path.
  • Blocklist additions and the generated Swift file are correctly in sync.
  • Test coverage is solid for the new diagnostic API and the direct-exec rejection paths, but the shell-wrapper behavior under blocked overrides is not exercised.

Confidence Score: 3/5

  • Directionally correct security hardening, but the node-host path has an architectural inconsistency that warrants review before merge.
  • The gateway path is well-implemented and the blocklist expansion is sound. The node-host path has two concerns: (1) sanitizeHostExecEnvWithDiagnostics builds a full merged env that is immediately discarded — the actual execution env uses a separate opts.sanitizeEnv path, creating a subtle divergence between the check and execution contexts; (2) the shell-wrapper sub-path silently drops blocked overrides rather than rejecting them, inconsistent with the stated "fail closed" goal. Neither is an immediate exploit, but they create ambiguity about what the security invariants actually are and could mask issues in future changes.
  • src/node-host/invoke-system-run.ts — the diagnostic sanitization call discards its computed env and doesn't cover the shell-wrapper sub-path.

Comments Outside Diff (1)

  1. src/node-host/invoke-system-run.ts, line 250-257 (link)

    P1 Shell-wrapper path silently drops blocked keys instead of failing closed

    When shellPayload !== null (i.e., a shell wrapper invocation), sanitizeSystemRunEnvOverrides filters opts.params.env down to only the shell-wrapper-allowed keys (TERM, LANG, LC_ALL, etc.) and returns that as envOverrides. As a result, a blocked key like CLASSPATH is silently discarded before sanitizeHostExecEnvWithDiagnostics even sees it — so the new rejection check never fires for that path.

    This means the "fail closed on blocked override keys" behavior only applies to the direct-exec (non-shell-wrapper) path. In the shell-wrapper case, a caller passing CLASSPATH: "/tmp/evil" receives a silent drop rather than an explicit error. This inconsistency is worth documenting or addressing: either the shell-wrapper case should also reject (mirroring the non-shell-wrapper behavior), or the intent to silently drop in this path should be noted with a comment, and the new test coverage should explicitly exercise the shell-wrapper scenario to confirm the expected behavior.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/node-host/invoke-system-run.ts
    Line: 250-257
    
    Comment:
    **Shell-wrapper path silently drops blocked keys instead of failing closed**
    
    When `shellPayload !== null` (i.e., a shell wrapper invocation), `sanitizeSystemRunEnvOverrides` filters `opts.params.env` down to only the shell-wrapper-allowed keys (`TERM`, `LANG`, `LC_ALL`, etc.) and returns that as `envOverrides`. As a result, a blocked key like `CLASSPATH` is silently discarded before `sanitizeHostExecEnvWithDiagnostics` even sees it — so the new rejection check never fires for that path.
    
    This means the "fail closed on blocked override keys" behavior only applies to the direct-exec (non-shell-wrapper) path. In the shell-wrapper case, a caller passing `CLASSPATH: "/tmp/evil"` receives a silent drop rather than an explicit error. This inconsistency is worth documenting or addressing: either the shell-wrapper case should also reject (mirroring the non-shell-wrapper behavior), or the intent to silently drop in this path should be noted with a comment, and the new test coverage should explicitly exercise the shell-wrapper scenario to confirm the expected behavior.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/node-host/invoke-system-run.ts
Line: 254-283

Comment:
**Computed env is built but immediately discarded**

`sanitizeHostExecEnvWithDiagnostics` is called here without a `baseEnv`, so it defaults to `process.env` and builds a full merged env object internally. However, `envSanitizeResult.env` is never referenced — only the rejection diagnostic arrays (`rejectedOverrideBlockedKeys`, `rejectedOverrideInvalidKeys`) are consumed. The actual execution env is still produced separately by `opts.sanitizeEnv(envOverrides)` on line 296.

This means every call through this path incurs the cost of building a merged env from `process.env` + accepted overrides only to throw it away. More importantly, it creates a subtle divergence: the diagnostic check implicitly uses `process.env` as its base, while the actual env used for execution is computed by a different code path (`opts.sanitizeEnv`). If those two paths ever diverge in their understanding of the base env, the rejection check could pass for a key that `opts.sanitizeEnv` would also allow.

Consider either:
- Passing the diagnostic result's `env` to `opts.sanitizeEnv` is replaced (since `sanitizeHostExecEnvWithDiagnostics` already produces a sanitized merged env), or
- Extracting only the override diagnostics without building the full merged env (e.g., a narrower helper that checks overrides without merging against a base).

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/node-host/invoke-system-run.ts
Line: 250-257

Comment:
**Shell-wrapper path silently drops blocked keys instead of failing closed**

When `shellPayload !== null` (i.e., a shell wrapper invocation), `sanitizeSystemRunEnvOverrides` filters `opts.params.env` down to only the shell-wrapper-allowed keys (`TERM`, `LANG`, `LC_ALL`, etc.) and returns that as `envOverrides`. As a result, a blocked key like `CLASSPATH` is silently discarded before `sanitizeHostExecEnvWithDiagnostics` even sees it — so the new rejection check never fires for that path.

This means the "fail closed on blocked override keys" behavior only applies to the direct-exec (non-shell-wrapper) path. In the shell-wrapper case, a caller passing `CLASSPATH: "/tmp/evil"` receives a silent drop rather than an explicit error. This inconsistency is worth documenting or addressing: either the shell-wrapper case should also reject (mirroring the non-shell-wrapper behavior), or the intent to silently drop in this path should be noted with a comment, and the new test coverage should explicitly exercise the shell-wrapper scenario to confirm the expected behavior.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "Exec: harden host en..."

Comment thread src/node-host/invoke-system-run.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f706096e3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/infra/host-env-security.ts Outdated
Comment thread src/node-host/invoke-system-run.ts Outdated
@joshavant

Copy link
Copy Markdown
Contributor Author

Follow-up pushed in ff0c326 to address review feedback:

  • Validate env override diagnostics on raw params.env before shell-wrapper filtering in src/node-host/invoke-system-run.ts.
  • Use override-only diagnostics (inspectHostExecEnvOverrides) in node parse flow to avoid building/discarding a merged env.
  • Preserve inherited non-portable Windows-style keys in host env sanitization (ProgramFiles(x86) regression case).
  • Add regressions for shell-wrapper blocked-key rejection and invalid-key rejection in src/node-host/invoke-system-run.test.ts.
  • Add inherited non-portable key coverage in src/infra/host-env-security.test.ts and src/node-host/invoke.sanitize-env.test.ts.

Targeted tests run:
pnpm test -- src/infra/host-env-security.test.ts src/node-host/invoke.sanitize-env.test.ts src/node-host/invoke-system-run.test.ts src/agents/bash-tools.exec.path.test.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff0c326a2b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/infra/host-env-security.ts Outdated
Comment thread src/node-host/invoke-system-run.ts
@joshavant
joshavant force-pushed the feat/host-env-override-hardening-phase1 branch from cbc3b05 to 1de1487 Compare March 20, 2026 20:43
Signed-off-by: joshavant <[email protected]>
@joshavant
joshavant merged commit 7abfff7 into main Mar 20, 2026
3 checks passed
@joshavant
joshavant deleted the feat/host-env-override-hardening-phase1 branch March 20, 2026 20:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fb2b239367

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"GEM_PATH",
"BUNDLE_GEMFILE",
"COMPOSER_HOME",
"XDG_CONFIG_HOME"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't strip inherited XDG_CONFIG_HOME from host execs

Adding XDG_CONFIG_HOME to blockedKeys means both sanitizeHostExecEnvWithDiagnostics() in src/infra/host-env-security.ts and the macOS HostEnvSanitizer.sanitize() now remove it from the inherited process environment for every gateway/node execution, even when the request did not ask for any env override. In deployments that start OpenClaw with a non-default XDG config root (common in containers and service units), commands like git, gh, or ssh will suddenly run against the wrong config or logged-out state. If the goal here is to reject request-scoped poisoning, this key needs to be blocked only for overrides, not for inherited host env.

Useful? React with 👍 / 👎.

frankekn pushed a commit to artwalker/openclaw that referenced this pull request Mar 23, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
furaul pushed a commit to furaul/openclaw that referenced this pull request Mar 24, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
Nachx639 pushed a commit to Nachx639/clawdbot that referenced this pull request Jun 17, 2026
…claw#51207)

* Exec: harden host env override enforcement and fail closed

* Node host: enforce env override diagnostics before shell filtering

* Env overrides: align Windows key handling and mac node rejection
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: macos App: macos maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant