nextlevelbuilder / nextlevelbuilder/goclaw

[Security] GoClaw `execApproval` Safe-Bin Classification Bypasses Human Approval for `sort -o` File Writes and `grep -R` Recursive Reads

Open
#1,206 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

agent:github-maintain area:security maintain:triaged P1-high
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 -o option 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:

  1. Start a local GoClaw instance in SQLite mode with:
    • tools.execApproval.security = "full"
    • tools.execApproval.ask = "on-miss"
  2. Connect to the official WebSocket RPC interface to monitor exec.approval.list.
  3. Send touch should-require-approval.txt as a control request through POST /v1/tools/invoke.
  4. Confirm the control request creates a pending approval and is denied when explicitly rejected.
  5. Send sort -o sort-output.txt input.txt and grep -R SAFE_BINS_STDIN_ONLY_BYPASS_CANARY . through the same HTTP API.
  6. 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.txt created a pending approval.
    • The request returned command denied by user after denial.
  • Exploit path:
    • sort-output.txt was created on disk with sorted content a\nb\n.
    • The grep response returned ./secret.txt:SAFE_BINS_STDIN_ONLY_BYPASS_CANARY.
    • Server logs recorded approval requests only for the control touch command, not for sort -o or grep -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.security set to full.
  • tools.execApproval.ask set to on-miss.
  • The exec builtin enabled.
  • Python 3 and the websocket-client dependency available for the PoC client scripts.
Reproduction Steps
  1. Download the minimal environment config from: config_min.json
  2. Download the minimal runner from: run_verification_min.sh
  3. Download the exploit verifier from: verification_min.py
  4. Download the control script from: control_min.py
  5. Place the files under the local PoC directory and start the environment:
    bash run_verification_min.sh /root/project/xclaw-project/goclaw
  6. Observe that the control command touch should-require-approval.txt is blocked behind a pending approval.
  7. Observe that the exploit verifier submits:
    • sort -o sort-output.txt input.txt
    • grep -R SAFE_BINS_STDIN_ONLY_BYPASS_CANARY .
  8. 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 exec tool.

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
Permalink Description
https://github.com/nextlevelbuilder/goclaw/blob/d85bf17171fd0faefbbd54df44bef573991aa7f8/internal/http/tools_invoke.go#L48-L55 The public HTTP entry point accepts authenticated operator-level callers for direct tool invocation.
https://github.com/nextlevelbuilder/goclaw/blob/d85bf17171fd0faefbbd54df44bef573991aa7f8/internal/http/tools_invoke.go#L117-L128 User-controlled args are forwarded into the tool registry, allowing the attacker-supplied command to reach the exec tool.
https://github.com/nextlevelbuilder/goclaw/blob/d85bf17171fd0faefbbd54df44bef573991aa7f8/internal/tools/exec_approval.go#L60-L76 safeBins unconditionally classifies sort and grep as safe commands.
https://github.com/nextlevelbuilder/goclaw/blob/d85bf17171fd0faefbbd54df44bef573991aa7f8/internal/tools/exec_approval.go#L134-L145 In ExecSecurityFull with ExecAskOnMiss, any command whose basename matches safeBins is auto-allowed instead of queued for approval.
https://github.com/nextlevelbuilder/goclaw/blob/d85bf17171fd0faefbbd54df44bef573991aa7f8/internal/tools/shell.go#L395-L408 The exec tool trusts the approval manager result and proceeds to execution when CheckCommand() returns allow.

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/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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.