home intel cve-2026-41380-openclaw-exec-approval-wrapper-bypass
CVE Analysis 2026-04-28 · 7 min read

CVE-2026-41380: OpenClaw Allowlist Bypass via Dispatch Wrapper Carrier Routing

OpenClaw's exec-approvals-allowlist.ts trusts the carrier executable rather than the invoked target, letting dispatch wrappers register overly broad allow-always entries. CVSS 7.3.

#execution-approval-bypass#allowlist-bypass#privilege-escalation#code-execution#wrapper-exploitation
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-41380 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-41380HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

CVE-2026-41380 is an execution approval logic flaw in OpenClaw affecting all versions prior to 2026.3.28. The vulnerability lives in exec-approvals-allowlist.ts, specifically in the code path that records allow-always decisions. When an executable is invoked through a dispatch wrapper (e.g., a shell, an interpreter, or any other carrier binary), OpenClaw's approval subsystem records the carrier — the wrapper — as the trusted entity rather than the target — the actual payload being executed. A subsequent invocation of any arbitrary executable through the same wrapper inherits that pre-approved status, defeating the entire purpose of per-binary allowlisting.

No public exploitation has been confirmed as of this writing, but the primitive is straightforward: convince a user to approve a benign executable once through a common wrapper (/usr/bin/env, sh -c, python3 -c), then route arbitrary code through the same wrapper transparently.

Root cause: buildAllowlistEntry() in exec-approvals-allowlist.ts keys the persistent trust record on process.execPath (the carrier/wrapper) rather than on the resolved target executable passed as the first positional argument, allowing any binary routed through an already-trusted wrapper to execute without re-prompting.

Affected Component

The vulnerable logic is entirely contained within the TypeScript execution approval subsystem:

  • File: src/exec-approvals-allowlist.ts
  • Function: buildAllowlistEntry() / persistApproval()
  • Mechanism: allow-always persistent trust grant
  • Fixed in: OpenClaw 2026.3.28 (commit resolves positional argument extraction before key derivation)

Root Cause Analysis

OpenClaw prompts users for execution approval and, when granted with allow-always, persists that decision to an on-disk allowlist. The trust key is supposed to identify the specific executable being approved. The bug is that for wrapper-dispatched invocations the key derivation reads spawnArgs.executablePath — which resolves to the wrapper — instead of examining spawnArgs.args[0], the actual target.


// Pseudocode reconstruction of exec-approvals-allowlist.ts: buildAllowlistEntry()
// Compiled TypeScript -> JS, logic represented as pseudo-C for clarity.

typedef struct {
    char *executablePath;   // path of the process being spawned (carrier/wrapper)
    char **args;            // argv array: args[0] is the real target when using wrappers
    int   argCount;
    bool  allowAlways;
} SpawnApprovalRequest;

AllowlistEntry buildAllowlistEntry(SpawnApprovalRequest *req) {
    AllowlistEntry entry;

    // BUG: uses req->executablePath unconditionally — this is the WRAPPER,
    // not the invoked target when dispatch wrappers like /usr/bin/env are used.
    entry.trustedPath = resolve_canonical(req->executablePath);

    // The actual target (e.g. /tmp/evil.sh) lives at req->args[0] but is
    // never consulted here. Any binary routed through an already-trusted
    // wrapper inherits the allow-always grant.
    entry.grantType   = req->allowAlways ? GRANT_ALWAYS : GRANT_ONCE;
    entry.timestamp   = unix_now();

    return entry;  // persisted to ~/.openclaw/exec-allowlist.json
}

// Caller: persistApproval()
void persistApproval(SpawnApprovalRequest *req) {
    if (!req->allowAlways) return;

    AllowlistEntry e = buildAllowlistEntry(req);  // BUG: wrong key derived here
    allowlist_write(e);                            // saved to disk permanently
}

// Checker path: isApproved()
bool isApproved(SpawnApprovalRequest *req) {
    char *key = resolve_canonical(req->executablePath);  // BUG: same wrong key

    // Lookup succeeds for ANY target routed through previously-approved wrapper.
    return allowlist_contains(key, GRANT_ALWAYS);
}

The structural problem is that executablePath and the logical executable are conflated. For direct invocations they are identical, which is why the bug was not caught: /usr/bin/cat invoked directly has executablePath == /usr/bin/cat and args[0] == /usr/bin/cat. But for any wrapper pattern — /usr/bin/env target, /bin/sh -c target, node runner.js target — they diverge.

Exploitation Mechanics


EXPLOIT CHAIN — CVE-2026-41380

Phase 1: Seed the allowlist with a legitimate carrier approval

1. Attacker (or social engineering target) invokes a benign executable through
   a common dispatch wrapper, e.g.:
       /usr/bin/env  /usr/local/bin/legitimate-tool

2. OpenClaw approval prompt fires. User clicks "Allow Always" for
   "legitimate-tool". Internally, buildAllowlistEntry() is called with:
       req->executablePath = "/usr/bin/env"       <-- carrier
       req->args[0]        = "/usr/local/bin/legitimate-tool"

3. BUG: allowlist key is derived from "/usr/bin/env", NOT from
   "/usr/local/bin/legitimate-tool". Disk entry written:
       { "trustedPath": "/usr/bin/env", "grant": "allow-always" }

Phase 2: Exploit the poisoned allowlist entry

4. Attacker arranges execution of arbitrary payload through same wrapper:
       /usr/bin/env  /tmp/attacker_payload.sh

5. isApproved() checks key = resolve_canonical("/usr/bin/env") -> MATCH.
   allow-always grant returned. No prompt shown. Payload executes silently.

6. Any subsequent execution routed through /usr/bin/env is permanently
   trusted for the lifetime of the allowlist entry — no further interaction
   required.

Phase 3: Lateral expansion (optional)

7. Additional wrappers approved through normal use (sh, bash, python3,
   node) each independently expand the attack surface. A single "Allow
   Always" click per wrapper permanently unlocks it as an execution
   vector.

Impact: Execution approval boundary fully bypassed for all wrapper-dispatched
        binaries. allow-always entries cannot be scoped to specific targets.

The exploit requires no memory corruption, no privilege escalation, and no kernel interaction. It is a pure logic flaw: one legitimate user approval creates a persistent, unconditionally broad trust grant for an entire dispatch class.

Allowlist State Diagram


ALLOWLIST STATE — INTENDED BEHAVIOR:

  User approves: /usr/bin/env /usr/local/bin/legitimate-tool
  ┌─────────────────────────────────────────────────────┐
  │ exec-allowlist.json                                 │
  │  { trustedPath: "/usr/local/bin/legitimate-tool",   │  <-- target keyed
  │    grant: "allow-always" }                          │
  └─────────────────────────────────────────────────────┘
  Query: /usr/bin/env /tmp/evil.sh   → KEY="/tmp/evil.sh"  → NO MATCH → PROMPT

─────────────────────────────────────────────────────────────

ALLOWLIST STATE — VULNERABLE BEHAVIOR (pre-2026.3.28):

  User approves: /usr/bin/env /usr/local/bin/legitimate-tool
  ┌─────────────────────────────────────────────────────┐
  │ exec-allowlist.json                                 │
  │  { trustedPath: "/usr/bin/env",                     │  <-- BUG: carrier keyed
  │    grant: "allow-always" }                          │
  └─────────────────────────────────────────────────────┘
  Query: /usr/bin/env /tmp/evil.sh   → KEY="/usr/bin/env"  → MATCH  → SILENT EXEC

  Query: /usr/bin/env /bin/rm -rf /  → KEY="/usr/bin/env"  → MATCH  → SILENT EXEC
  Query: /usr/bin/env /tmp/c2agent   → KEY="/usr/bin/env"  → MATCH  → SILENT EXEC

BLAST RADIUS: Every binary dispatched via /usr/bin/env is permanently trusted.

Patch Analysis

The fix in 2026.3.28 introduces a resolver that, given a SpawnApprovalRequest, extracts the logical target before deriving the allowlist key. For wrapper invocations, this means walking args[0] rather than trusting executablePath.


// BEFORE (vulnerable — pre-2026.3.28):
AllowlistEntry buildAllowlistEntry(SpawnApprovalRequest *req) {
    AllowlistEntry entry;
    entry.trustedPath = resolve_canonical(req->executablePath);  // always the carrier
    entry.grantType   = req->allowAlways ? GRANT_ALWAYS : GRANT_ONCE;
    return entry;
}

bool isApproved(SpawnApprovalRequest *req) {
    char *key = resolve_canonical(req->executablePath);           // always the carrier
    return allowlist_contains(key, GRANT_ALWAYS);
}

// ─────────────────────────────────────────────────────────────────────────────

// AFTER (patched — 2026.3.28):
// New helper: resolves the LOGICAL executable, unwrapping carrier dispatch.
char *resolveLogicalTarget(SpawnApprovalRequest *req) {
    // Known wrapper binaries that take a target as their first positional arg.
    static const char *DISPATCH_WRAPPERS[] = {
        "/usr/bin/env", "/bin/env",
        "/bin/sh",  "/bin/bash", "/usr/bin/sh", "/usr/bin/bash",
        NULL
    };

    char *canon = resolve_canonical(req->executablePath);

    for (int i = 0; DISPATCH_WRAPPERS[i] != NULL; i++) {
        if (strcmp(canon, DISPATCH_WRAPPERS[i]) == 0) {
            // Carrier detected: key on args[0] (the actual target), not the wrapper.
            if (req->argCount > 0 && req->args[0] != NULL) {
                return resolve_canonical(req->args[0]);  // FIX: use real target
            }
        }
    }
    return canon;  // Direct invocation: executablePath IS the target.
}

AllowlistEntry buildAllowlistEntry(SpawnApprovalRequest *req) {
    AllowlistEntry entry;
    entry.trustedPath = resolveLogicalTarget(req);  // FIX: carrier-aware resolution
    entry.grantType   = req->allowAlways ? GRANT_ALWAYS : GRANT_ONCE;
    return entry;
}

bool isApproved(SpawnApprovalRequest *req) {
    char *key = resolveLogicalTarget(req);           // FIX: same resolution path
    return allowlist_contains(key, GRANT_ALWAYS);
}

The patch is logically sound for known wrapper patterns but relies on a static wrapper list. Unlisted carrier binaries (custom launchers, language runtimes invoked with -e/-c flags, xargs, sudo) are not covered and would still exhibit the original behavior. Downstream deployments using non-standard wrappers should audit their specific execution patterns against the wrapper list in the patched source.

Detection and Indicators

Because the vulnerability affects only the allowlist persistence logic, detection focuses on the on-disk allowlist and execution audit logs:


INDICATOR 1 — Suspicious allowlist entries
  File: ~/.openclaw/exec-allowlist.json
  Red flag: trustedPath resolves to a known dispatch wrapper binary:
    /usr/bin/env, /bin/sh, /bin/bash, /usr/bin/python3, /usr/bin/node
  Any allow-always entry keyed to a wrapper binary is malformed and
  indicates either exploitation or a system affected by the bug.

INDICATOR 2 — Execution without approval prompt
  OpenClaw audit log: ~/.openclaw/exec-audit.log
  Look for: approved=true, promptShown=false, executablePath=,
            args[0]=
  Specifically: wrapper previously approved, target binary is newly
                introduced or lives in a writable path (/tmp, /var/tmp,
                $HOME).

INDICATOR 3 — Approval timestamp anomalies
  If allowlist entry timestamp predates the introduction of the target
  binary on disk (mtime), the entry was created for a different target
  and is being leveraged for the current one.

YARA (conceptual — match on allowlist file content):
rule openclaw_wrapper_allowlist_poisoning {
    strings:
        $env  = "\"trustedPath\":\"/usr/bin/env\""
        $sh   = "\"trustedPath\":\"/bin/sh\""
        $bash = "\"trustedPath\":\"/bin/bash\""
        $ga   = "\"grant\":\"allow-always\""
    condition:
        ($env or $sh or $bash) and $ga
}

Remediation

  • Upgrade to OpenClaw ≥ 2026.3.28 immediately.
  • Scrub existing allowlists: Delete or audit ~/.openclaw/exec-allowlist.json on all affected systems. Any allow-always entry whose trustedPath resolves to a dispatch wrapper binary must be removed — it cannot be trusted.
  • Re-approve selectively: After upgrading, re-approve only the specific target executables you intend to trust. The patched build will correctly key entries on the resolved target.
  • Custom wrapper audit: If your environment uses launcher scripts or non-standard wrapper binaries not in the patched wrapper list, verify that those patterns produce correctly-keyed allowlist entries post-upgrade, or open an upstream issue to extend the wrapper detection list.
  • Defense-in-depth: Supplement OpenClaw's allowlisting with a file-integrity monitor on the wrapper binaries themselves (auditd, Falco) so that anomalous files dispatched through trusted wrappers generate independent alerts even if the approval prompt is suppressed.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →