nextlevelbuilder / nextlevelbuilder/goclaw
[Security] GoClaw `exec` Secure-CLI Gate Lets PowerShell `-EncodedCommand` Bypass Grant Enforcement
Nobody has claimed this yet.
- 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
sqliteonlybuild 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
exectool.
Reproduction Steps
- Download the minimal verification harness from: verification_harness.go
- Download the exploit driver from: verification_test.py
- Download the control driver from: control-normal-behavior.py
- Place the three files in the same directory.
- 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 - Observe that the command succeeds even though the inner
ghbinary is registered as a secure CLI without a grant. - 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 - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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