VYPR
High severityNVD Advisory· Published Mar 18, 2026· Updated Mar 25, 2026

OpenClaw < 2026.2.22 - Allowlist Bypass via Command Substitution in system.run

CVE-2026-22179

Description

OpenClaw versions prior to 2026.2.22 in macOS node-host system.run contain an allowlist bypass vulnerability that allows remote attackers to execute non-allowlisted commands by exploiting improper parsing of command substitution tokens. Attackers can craft shell payloads with command substitution syntax within double-quoted text to bypass security restrictions and execute arbitrary commands on the system.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.2.222026.2.22

Affected products

1

Patches

1
90a378ca3a9e

fix(macos): block quoted shell substitution in allowlist checks

https://github.com/openclaw/openclawPeter SteinbergerFeb 21, 2026via ghsa
9 files changed · +53 6
  • apps/macos/Sources/OpenClaw/ExecCommandResolution.swift+7 5 modified
    @@ -194,11 +194,13 @@ struct ExecCommandResolution: Sendable {
                     continue
                 }
     
    +            if !inSingle, self.shouldFailClosedForShell(ch: ch, next: next) {
    +                // Fail closed on command/process substitution in allowlist mode,
    +                // including inside double-quoted shell strings.
    +                return nil
    +            }
    +
                 if !inSingle, !inDouble {
    -                if self.shouldFailClosedForUnquotedShell(ch: ch, next: next) {
    -                    // Fail closed on command/process substitution in allowlist mode.
    -                    return nil
    -                }
                     let prev: Character? = idx > 0 ? chars[idx - 1] : nil
                     if let delimiterStep = self.chainDelimiterStep(ch: ch, prev: prev, next: next) {
                         guard appendCurrent() else { return nil }
    @@ -216,7 +218,7 @@ struct ExecCommandResolution: Sendable {
             return segments
         }
     
    -    private static func shouldFailClosedForUnquotedShell(ch: Character, next: Character?) -> Bool {
    +    private static func shouldFailClosedForShell(ch: Character, next: Character?) -> Bool {
             if ch == "`" {
                 return true
             }
    
  • apps/macos/Tests/OpenClawIPCTests/ExecAllowlistTests.swift+20 0 modified
    @@ -80,6 +80,26 @@ struct ExecAllowlistTests {
             #expect(resolutions.isEmpty)
         }
     
    +    @Test func resolveForAllowlistFailsClosedOnQuotedCommandSubstitution() {
    +        let command = ["/bin/sh", "-lc", "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\""]
    +        let resolutions = ExecCommandResolution.resolveForAllowlist(
    +            command: command,
    +            rawCommand: "echo \"ok $(/usr/bin/touch /tmp/openclaw-allowlist-test-quoted-subst)\"",
    +            cwd: nil,
    +            env: ["PATH": "/usr/bin:/bin"])
    +        #expect(resolutions.isEmpty)
    +    }
    +
    +    @Test func resolveForAllowlistFailsClosedOnQuotedBackticks() {
    +        let command = ["/bin/sh", "-lc", "echo \"ok `/usr/bin/id`\""]
    +        let resolutions = ExecCommandResolution.resolveForAllowlist(
    +            command: command,
    +            rawCommand: "echo \"ok `/usr/bin/id`\"",
    +            cwd: nil,
    +            env: ["PATH": "/usr/bin:/bin"])
    +        #expect(resolutions.isEmpty)
    +    }
    +
         @Test func resolveForAllowlistTreatsPlainShInvocationAsDirectExec() {
             let command = ["/bin/sh", "./script.sh"]
             let resolutions = ExecCommandResolution.resolveForAllowlist(
    
  • apps/macos/Tests/OpenClawIPCTests/GatewayChannelConfigureTests.swift+4 0 modified
    @@ -70,6 +70,10 @@ import Testing
                 handler?(Result<URLSessionWebSocketTask.Message, Error>.success(.data(response)))
             }
     
    +        func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
    +            pongReceiveHandler(nil)
    +        }
    +
             func receive() async throws -> URLSessionWebSocketTask.Message {
                 if self.helloDelayMs > 0 {
                     try await Task.sleep(nanoseconds: UInt64(self.helloDelayMs) * 1_000_000)
    
  • apps/macos/Tests/OpenClawIPCTests/GatewayChannelConnectTests.swift+4 0 modified
    @@ -53,6 +53,10 @@ import Testing
                 }
             }
     
    +        func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
    +            pongReceiveHandler(nil)
    +        }
    +
             func receive() async throws -> URLSessionWebSocketTask.Message {
                 let delayMs: Int
                 let msg: URLSessionWebSocketTask.Message
    
  • apps/macos/Tests/OpenClawIPCTests/GatewayChannelRequestTests.swift+4 0 modified
    @@ -62,6 +62,10 @@ import Testing
                 }
             }
     
    +        func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
    +            pongReceiveHandler(nil)
    +        }
    +
             func receive() async throws -> URLSessionWebSocketTask.Message {
                 let id = self.connectRequestID.withLock { $0 } ?? "connect"
                 return .data(Self.connectOkData(id: id))
    
  • apps/macos/Tests/OpenClawIPCTests/GatewayChannelShutdownTests.swift+4 0 modified
    @@ -47,6 +47,10 @@ import Testing
                 }
             }
     
    +        func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
    +            pongReceiveHandler(nil)
    +        }
    +
             func receive() async throws -> URLSessionWebSocketTask.Message {
                 let id = self.connectRequestID.withLock { $0 } ?? "connect"
                 return .data(Self.connectOkData(id: id))
    
  • apps/macos/Tests/OpenClawIPCTests/GatewayConnectionControlTests.swift+4 0 modified
    @@ -15,6 +15,10 @@ private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
     
         func send(_: URLSessionWebSocketTask.Message) async throws {}
     
    +    func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
    +        pongReceiveHandler(nil)
    +    }
    +
         func receive() async throws -> URLSessionWebSocketTask.Message {
             throw URLError(.cannotConnectToHost)
         }
    
  • apps/macos/Tests/OpenClawIPCTests/GatewayProcessManagerTests.swift+4 0 modified
    @@ -64,6 +64,10 @@ struct GatewayProcessManagerTests {
                 handler?(Result<URLSessionWebSocketTask.Message, Error>.success(.data(response)))
             }
     
    +        func sendPing(pongReceiveHandler: @escaping @Sendable (Error?) -> Void) {
    +            pongReceiveHandler(nil)
    +        }
    +
             func receive() async throws -> URLSessionWebSocketTask.Message {
                 let id = self.connectRequestID.withLock { $0 } ?? "connect"
                 return .data(Self.connectOkData(id: id))
    
  • apps/macos/Tests/OpenClawIPCTests/MacGatewayChatTransportMappingTests.swift+2 1 modified
    @@ -13,7 +13,8 @@ import Testing
                 configpath: nil,
                 statedir: nil,
                 sessiondefaults: nil,
    -            authmode: nil)
    +            authmode: nil,
    +            updateavailable: nil)
     
             let hello = HelloOk(
                 type: "hello",
    

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

5

News mentions

0

No linked articles in our index yet.