nextlevelbuilder / nextlevelbuilder/goclaw
[Security] GoClaw `execApproval` Safe-Bin Classification Bypasses Human Approval for `sort -o` File Writes and `grep -R` Recursive Reads
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 execApproval Safe-Bin Classification Bypasses Human Approval for sort -o File Writes and grep -R Recursive Reads
Description:
Summary
GoClaw exposes a public exec tool through POST /v1/tools/invoke for authenticated operator-capable users. When deployments enable tools.execApproval.security = "full" together with tools.execApproval.ask = "on-miss", the intended safety model is that clearly safe commands are auto-approved while higher-risk commands require explicit human approval. In affected versions, the approval manager trusts certain binaries solely by basename via safeBins, and it classifies both sort and grep as always safe. Because the approval decision ignores option semantics, an authenticated operator can use sort -o ... to write files and grep -R ... to recursively read workspace contents without generating a pending approval request.
Details
The reachable attack path starts at the public HTTP handler for direct tool invocation. ToolsInvokeHandler.ServeHTTP() authenticates the request and accepts callers with at least RoleOperator, then forwards attacker-controlled JSON args to 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, execution reaches ExecTool.Execute(), which delegates approval decisions to ExecApprovalManager.CheckCommand(command) before invoking the host shell.
if t.approvalMgr != nil {
switch t.approvalMgr.CheckCommand(command) {
case "deny":
return ErrorResult("command denied by exec approval policy")
case "ask":
decision, err := t.approvalMgr.RequestApproval(command, t.agentID, 2*time.Minute)
...
}
}
The defect is in ExecApprovalManager.CheckCommand(). Under ExecSecurityFull + ExecAskOnMiss, the code auto-allows any command whose basename matches safeBins:
case ExecSecurityFull:
switch m.config.Ask {
case ExecAskOnMiss:
if m.matchesAllowlist(command) || m.isSafeBin(command) {
return "allow"
}
return "ask"
}
At the same time, safeBins includes both sort and grep unconditionally:
"tail": true, "wc": true, "sort": true, "uniq": true, "grep": true,
That logic treats the following very differently from the intended approval model:
sort -o sort-output.txt input.txt
This is not a read-only text operation. The-ooption writes a new file inside the workspace.grep -R SAFE_BINS_STDIN_ONLY_BYPASS_CANARY .
This is not a narrow stdin-only grep. The recursive flag walks the workspace tree and discloses matching file content.
I verified this end-to-end against the real gateway using only supported interfaces:
- Start a local GoClaw instance in SQLite mode with:
tools.execApproval.security = "full"tools.execApproval.ask = "on-miss"
- Connect to the official WebSocket RPC interface to monitor
exec.approval.list. - Send
touch should-require-approval.txtas a control request throughPOST /v1/tools/invoke. - Confirm the control request creates a pending approval and is denied when explicitly rejected.
- Send
sort -o sort-output.txt input.txtandgrep -R SAFE_BINS_STDIN_ONLY_BYPASS_CANARY .through the same HTTP API. - Confirm both exploit commands execute successfully with no pending approval created.
The runtime evidence is consistent with a real approval bypass rather than a false positive:
- Control path:
touch should-require-approval.txtcreated a pending approval.- The request returned
command denied by userafter denial.
- Exploit path:
sort-output.txtwas created on disk with sorted contenta\nb\n.- The grep response returned
./secret.txt:SAFE_BINS_STDIN_ONLY_BYPASS_CANARY. - Server logs recorded approval requests only for the control
touchcommand, not forsort -oorgrep -R.
This is therefore a genuine bypass of an advertised host-command approval control. The vulnerable behavior is reachable through standard product interfaces and produces real unauthorized file write/read effects within the normal exec tool scope.
PoC
Prerequisites
- A GoClaw deployment built from the repository.
- An authenticated Bearer token with at least operator-level access to
POST /v1/tools/invoke. tools.execApproval.securityset tofull.tools.execApproval.askset toon-miss.- The
execbuiltin enabled. - Python 3 and the
websocket-clientdependency available for the PoC client scripts.
Reproduction Steps
- Download the minimal environment config from: config_min.json
- Download the minimal runner from: run_verification_min.sh
- Download the exploit verifier from: verification_min.py
- Download the control script from: control_min.py
- Place the files under the local PoC directory and start the environment:
bash run_verification_min.sh /root/project/xclaw-project/goclaw - Observe that the control command
touch should-require-approval.txtis blocked behind a pending approval. - Observe that the exploit verifier submits:
sort -o sort-output.txt input.txtgrep -R SAFE_BINS_STDIN_ONLY_BYPASS_CANARY .
- Confirm the exploit succeeds without any pending approval request for either command.
Log of Evidence
Verification mode: End-to-End
[CONTROL-BLOCKED]
pending_count=1
pending_commands=['touch should-require-approval.txt']
control_http_result='status=400 body={"error":{"code":"TOOL_ERROR","message":"command denied by user"}}'
Verification mode: End-to-End
[DEFECT-CONFIRMED]
control: touch -> pending approval created
sort_file='a\nb\n'
sort_output='(command completed with no output)'
grep_output='./secret.txt:SAFE_BINS_STDIN_ONLY_BYPASS_CANARY\n'
# Independent Observation
## Workspace Files
input.txt 4 bytes
secret.txt 35 bytes
sort-output.txt 4 bytes
## sort-output.txt
a
b
## secret.txt grep check
/tmp/goclaw-cve-2026-31996/workspace/secret.txt:SAFE_BINS_STDIN_ONLY_BYPASS_CANARY
## Approval Request Log
86:time=2026-06-10T15:19:41.424+08:00 level=INFO msg="exec approval requested" id=exec-1 command="touch should-require-approval.txt"
91:time=2026-06-10T15:19:42.291+08:00 level=INFO msg="exec approval requested" id=exec-2 command="touch should-require-approval.txt"
Impact
This is an authorization/control-bypass issue in the host command approval layer. It impacts deployments that intentionally rely on ask=on-miss to insert a human decision point before risky shell actions execute. An authenticated operator can silently bypass that approval boundary by choosing a basename that the product hard-codes as safe and then adding options that introduce filesystem read or write behavior.
In practical terms, impacted users can:
- Write or overwrite files inside the allowed workspace without a pending approval dialog by using
sort -o. - Recursively read workspace contents and disclose matching file data without approval by using
grep -R. - Defeat the operator-review workflow that administrators expect to protect the
exectool.
The vulnerability does not require local patching, debug hooks, or private APIs. It works through the documented HTTP and WebSocket interfaces under supported configuration.
Affected products
- Ecosystem: go
- Package name: github.com/nextlevelbuilder/goclaw
- Affected versions: <= 3.13.2
- Patched versions:
Severity
- Severity: Medium
- Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N
Weaknesses
- CWE: CWE-285: Improper Authorization
Occurrences
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/exec_approval.go, especially safeBins and CheckCommand(), then trace the request path through internal/http/tools_invoke.go and internal/tools/shell.go. Reproduce the listed sort -o and grep -R commands under full security with on-miss approval, using the provided verification scripts. Done means these option-bearing commands no longer bypass the approval request while intended safe commands retain their behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- api, backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100