OpenClaw is a software tool that helps control which programs are allowed to run on your computer. Think of it like a bouncer at a club, checking IDs before letting people in. This vulnerability is a flaw in how that bouncer checks those IDs.
Here's the problem: attackers have found a way to trick the system by using a middleman. Imagine you have a trusted delivery driver (a legitimate program) who's allowed to enter your house. A bad actor could dress up as that driver, or pretend to be calling on behalf of that driver, to get inside without proper verification. The security system sees "trusted delivery driver" and lets them through, without realizing it's someone else actually doing the work.
In technical terms, the system approves "wrapper" programs (the middlemen) instead of checking what those wrappers actually end up running. This means someone could use a trusted program as cover to secretly execute malicious code on your machine.
Who should worry? Developers and companies using OpenClaw to manage what software runs on their systems are most at risk. If you use OpenClaw at work, your IT team should know about this. Home users probably aren't directly affected unless they actively use this software.
The good news is nobody has actively exploited this yet, so there's a window to fix it.
What you should do: First, if your workplace uses OpenClaw, alert your IT or security team immediately. Second, update to version 2026.3.28 or later as soon as it's available and tested by your organization. Third, have your IT team review which programs are currently whitelisted to make sure the list isn't too broad.
Want the full technical analysis? Click "Technical" above.
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:
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.