nextlevelbuilder / nextlevelbuilder/goclaw

[Security] GoClaw `exec` Secure-CLI Gate Lets PowerShell `-EncodedCommand` Bypass Grant Enforcement

Open
#1,215 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

agent:github-maintain area:security maintain:triaged P1-high status:blocked
Dominant language
Go
Stars
3.6k
Forks
1.1k
Avg merge
3d 5h
Merged PRs (30d)
24

Description

Advisory Details

Title: GoClaw exec Secure-CLI Gate Lets PowerShell -EncodedCommand Bypass Grant Enforcement

Description:

Summary

GoClaw exposes a direct exec tool to authenticated operator-level callers and agents. Deployments can register high-risk binaries such as gh as tenant-scoped secure CLIs so those binaries require an explicit grant before execution. In affected versions, the wrapper-unwrapping logic recognizes several POSIX-style launchers but does not recognize PowerShell wrapper forms such as pwsh -EncodedCommand. As a result, an authenticated caller can hide a registered secure CLI inside PowerShell-encoded input and execute it without the intended secure-CLI grant.

Details

The reachable attack path starts at the public direct tool invocation endpoint. ToolsInvokeHandler.ServeHTTP() accepts authenticated callers with at least RoleOperator and forwards user-controlled arguments into the tool registry:

auth := resolveAuth(r)
if !auth.Authenticated {
    ...
}
if !permissions.HasMinRole(auth.Role, permissions.RoleOperator) {
    ...
}
...
result := h.registry.ExecuteWithContext(ctx, req.Tool, args, "http", "api", "direct", "", nil)

When tool=exec, the request reaches ExecTool.Execute(). The secure-CLI gate does not inspect arbitrary shell text directly; it first asks collectGateCandidates() to recursively unwrap known wrappers and then checks each discovered binary against the secure CLI store:

if t.secureCLIStore != nil {
    candidates, tooDeep := collectGateCandidates(normalizedCommand)
    ...
    for _, c := range candidates {
        ...
        registered, rerr := t.secureCLIStore.IsRegisteredBinary(gctx, c.binary)
        ...
        if registered {
            return ErrorResult(fmt.Sprintf(
                "Binary %q requires a secure CLI grant. Ask admin to grant access to this agent.",
                c.binary))
        }
    }
}

The bypass exists because wrapper recognition is incomplete. In internal/tools/credentialed_exec.go, the wrapperBinaries map contains only POSIX/utility wrappers:

var wrapperBinaries = map[string]bool{
    "sh": true, "bash": true, "zsh": true, "dash": true,
    "env": true, "nohup": true, "stdbuf": true, "timeout": true,
}

detectWrapper() immediately returns ok=false if the outer binary is not in that map:

head := normalizeBinaryName(words[0])
if !wrapperBinaries[head] {
    return "", "", false
}

Because neither pwsh nor powershell is recognized, a command such as:

pwsh -EncodedCommand Z2ggQ0FOQVJZX0JZUEFTUw==

is treated as if the only executable of interest were pwsh. The secure-CLI gate therefore checks whether pwsh is registered, not whether the decoded inner command gh CANARY_BYPASS is registered. If gh is a tenant-scoped secure CLI and the current agent has no grant, the protection still fails open for this wrapper form and host execution continues.

I verified this against the real ExecTool.Execute() implementation with the real SQLite secure CLI store and tenant context. The harness seeds gh into secure_cli_binaries, places PATH-local pwsh and gh binaries in front of the system PATH, then invokes the production code path. The experiment command:

pwsh -EncodedCommand <base64("gh CANARY_BYPASS")>

returned success and wrote the CANARY_BYPASS marker through the gh stub. A control command:

sh -c 'gh CANARY_CONTROL'

was denied with Binary "gh" requires a secure CLI grant and produced no control canary write. This shows the boundary is enforced for recognized wrappers but bypassed for PowerShell-encoded forms.

PoC
Prerequisites
  • A checkout of the GoClaw repository.
  • Go installed with support for the sqliteonly build tag.
  • Python 3 available to run the minimal wrapper scripts.
  • No external services are required.
  • In real deployments, an authenticated caller or agent capable of invoking the exec tool.
Reproduction Steps
  1. Download the minimal verification harness from: verification_harness.go
  2. Download the exploit driver from: verification_test.py
  3. Download the control driver from: control-normal-behavior.py
  4. Place the three files in the same directory.
  5. From the repository root, run the exploit case:
    python3 llm-enhance/cve-finding/similar/Auth_Bypass/GHSA-3h2q-j2v4-6w5r-powershell-secure-cli-wrapper-exp/verification_test.py
  6. Observe that the command succeeds even though the inner gh binary is registered as a secure CLI without a grant.
  7. Run the control case:
    python3 llm-enhance/cve-finding/similar/Auth_Bypass/GHSA-3h2q-j2v4-6w5r-powershell-secure-cli-wrapper-exp/control-normal-behavior.py
  8. Observe that the POSIX-shell wrapper path is denied and does not write the control marker.
Log of Evidence
Verification mode: Unit-Test
Interface available: CLI-like exec tool entrypoint inside real ExecTool.Execute
Command: pwsh -EncodedCommand Z2ggQ0FOQVJZX0JZUEFTUw==
Result: isError=false output="(command completed with no output)"
Independent observation: canary file contains bypass marker = true
[DEFECT-CONFIRMED-WITH-LIMITATIONS]
Verification mode: Unit-Test
Interface available: CLI-like exec tool entrypoint inside real ExecTool.Execute
Command: sh -c 'gh CANARY_CONTROL'
Result: isError=true output="Binary \"gh\" requires a secure CLI grant. Ask admin to grant access to this agent."
Independent observation: canary file contains control marker = false
[CONTROL-PROTECTED]
Impact

This is an authorization bypass in GoClaw's secure-CLI execution boundary. It affects deployments that rely on secure CLI registration and per-agent grants to prevent unauthorized use of high-risk host binaries. An authenticated operator or agent path that can invoke exec can run a tenant-registered secure CLI without a grant by wrapping it in PowerShell encoded-command syntax.

In practice, impacted users can:

  • execute registered credentialed/high-risk CLIs despite lacking the intended grant;
  • abuse any permissions exposed by the wrapped CLI, such as repository administration, cloud control-plane access, cluster operations, release actions, or other credential-backed automation;
  • bypass an administrator's expectation that secure CLI registration is sufficient to stop unauthorized execution through the official product interface.

The verified impact in this case is unauthorized execution of a registered gh binary through pwsh -EncodedCommand.

Affected products
  • Ecosystem: go
  • Package name: github.com/nextlevelbuilder/goclaw
  • Affected versions: <= 3.13.3-beta.3
  • Patched versions:
Severity
  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
Weaknesses
  • CWE: CWE-285: Improper Authorization
Occurrences
Permalink Description
https://github.com/nextlevelbuilder/goclaw/blob/7484d0ec74208641f4c623f983e5f820a1142a41/internal/http/tools_invoke.go#L48-L55 The public HTTP entry point allows authenticated operator-level callers to reach direct tool invocation.
https://github.com/nextlevelbuilder/goclaw/blob/7484d0ec74208641f4c623f983e5f820a1142a41/internal/http/tools_invoke.go#L117-L128 User-controlled tool arguments are forwarded into the registry, allowing attacker-controlled command text to reach the exec tool.
https://github.com/nextlevelbuilder/goclaw/blob/7484d0ec74208641f4c623f983e5f820a1142a41/internal/tools/credentialed_exec.go#L35-L38 wrapperBinaries omits pwsh and powershell, so PowerShell wrapper forms are not considered for secure-CLI unwrapping.
https://github.com/nextlevelbuilder/goclaw/blob/7484d0ec74208641f4c623f983e5f820a1142a41/internal/tools/credentialed_exec.go#L59-L69 detectWrapper() rejects any outer binary not present in wrapperBinaries, which causes pwsh -EncodedCommand ... to stop unwrapping before the inner secure CLI is inspected.
https://github.com/nextlevelbuilder/goclaw/blob/7484d0ec74208641f4c623f983e5f820a1142a41/internal/tools/shell.go#L360-L390 The secure-CLI gate trusts collectGateCandidates() output and denies only binaries that appear in the collected candidate list; when PowerShell wrapping is missed, the protected inner binary is never checked.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with internal/tools/credentialed_exec.go, especially wrapperBinaries and detectWrapper(), then trace how internal/tools/shell.go uses collectGateCandidates() for the secure-CLI gate. Reproduce the encoded PowerShell case with the verification harness and compare it with the protected POSIX-shell control. Done means registered inner binaries are denied without a grant while the existing control behavior remains protected.

Written by the indexing model from the issue text.

Assessment

Tech stack
go, powershell
Domain
api, authorization, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.