The Windows node's exec-approval policy limits what a connected gateway/agent may run on the host. system.execApprovals.set lets a caller update that policy, and ValidateExecApprovalRules is the hard backstop meant to stop a caller from weakening the policy into arbitrary execution. It explicitly tries to reject broad Allow rules — it blocks the all-wildcard shapes ( * , ** , ?* ), shell-blanket shapes ( powershell * , cmd * ), a defaultAction:"allow" , absolute-path patterns, and a denylist of dangerous command fragments. But the broad-rule check is a denylist of enumerated shapes , decoupled from how the policy actually matches: ExecApprovalPolicy.MatchesPattern compiles a rule to an anchored regex with * → .* over the whole command line . So any Allow pattern whose executable is wildcarded — *.* , *e* , *.exe , *x* , c* — passes the validator yet matches nearly every command. A .set caller adds one such rule, then system.run executes with no prompt. It is the same "compromise token → whitelist → invoke" EoP the check exists to stop, except the single rule grants all commands rather than one binary (CWE-863 / CWE-184).
No per-call admin gate. HandleExecApprovalsSet is guarded only by (a) the tray "Run system tools" permission toggle (whether run/run.prepare are exposed), (b) an optimistic-concurrency baseHash (readable via system.execApprovals.get ), and (c) ValidateExecApprovalRules . There is no operator-scope/admin authorization on the call itself, and no user prompt — the rules are validated and persisted ( SetRules ) directly.
The broad-rule check is shape-enumeration, decoupled from the matcher. For an Allow rule, ValidateExecApprovalRules strips * / ? and rejects only if what remains is empty (catches * , ** , ?* ), plus the literal shell-blanket set and absolute-path/LOLBin checks. Any pattern with even one non-wildcard character therefore passes — regardless of how broadly it actually matches.
The matcher makes those patterns near-blanket. MatchesPattern builds ^ + Regex.Escape(pattern) with \* → .* , \? → . , + $ , matched against the full command string . So *.* → ^.*\..*$ matches any command containing a dot (every real Windows invocation: cmd.exe … , powershell.exe … , payload.exe ); *e* → any command containing e ; c* → any command starting with c . The executable itself is wildcarded, so an arbitrary command matches.
This is a class, not a single pattern. *.* , *e* , *.exe , *x* , c* all bypass the validator (verified). Extending the enumerated shapes (as prior hardening did for LOLBins) cannot close it — the root cause is that the write-side check does not model the match-side * → .* breadth.
Once accepted, the policy allows it. After the accepted .set , ExecApprovalPolicy.Evaluate matches an arbitrary command against the new broad Allow rule (V1/legacy path, the enforced one — no V2 handler is installed), so system.run runs it. A command denied moments earlier now executes.
PoC (benign — nothing malicious is executed; a decoy exe and a fake command runner are used)
Driving the real system.execApprovals.set + system.run handlers on the compiled node ( SystemCapability , default policy defaultAction=Deny , fake command runner):
CWE-863 (Incorrect Authorization) / CWE-184 (Incomplete List of Disallowed Inputs). On a node where "Run system tools" is enabled, a gateway/agent that can call system.execApprovals.set — the "compromised MCP token" / lower-trust or prompt-injected caller the exec-approval boundary is meant to contain — can weaken the policy from "read-only diagnostics only" to "runs any command" with a single rule, then execute arbitrary code on the user's Windows host. This is strictly broader than whitelisting one binary: it neutralizes the entire exec-approval gate. Confidentiality, integrity, and availability of the host are all at risk.
Fix (implemented + verified; ready to push to the advisory fork)
Root-cause fix, not a new shape to enumerate: the executable an Allow rule names (the first whitespace-delimited token) must be a concrete literal — a wildcard where the command name goes is what lets a rule match any command. Placed after the dangerous-fragment check so a dangerous stem keeps its specific message. In ValidateExecApprovalRules (src/OpenClaw.Shared/Capabilities/SystemCapability.cs):
Verification: added regression tests (broad class *.* / *e* / *.exe / *x* / c* rejected; legit git * / npm run * / node *.js accepted; the end-to-end control→set→exec bypass now blocked). Full OpenClaw.Shared.Tests suite (2941 tests) passes to the pre-existing baseline — the fix adds zero regressions, and it preserves the existing dangerous-stem rejections ( rm* , del* , shutdown* ). Complementary to GHSA-g95v-c9r6-2hmq (the LOLBin denylist for named binaries); this closes the wildcarded-executable class the denylist cannot.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
