Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [day] [month] [year] [list]
Message-ID: <316D307A-0F82-4F1D-89B2-E02B957EE48A.1@smtp-inbound1.duck.com>
Date: Fri, 13 Nov 2026 14:38:21 -0500
From: theknell@...k.com
To: oss-security@...ts.openwall.com
Subject: Dropbear: the restriction names an address — permitopen/permitlisten not enforced for unix-socket forwarding, docker.sock reachable (HEAD 99baf182, live-proven)

Dropbear is the SSH server that ships where OpenSSH is too big: routers,
embedded systems, rescue images, appliances — anywhere a small, hardened
sshd is wanted. It has been fuzzed and audited for two decades, and its
handling of restricted keys is one of the reasons administrators trust it
with narrow jobs. A key in authorized_keys can carry permitopen="host:port"
and permitlisten="host:port": this key may forward to that destination,
and to no other. Backup keys, tunnel keys, CI keys are handed out on the
strength of that line.

The restriction names an address. A unix socket is not an address, and on
the socket path the restriction is never consulted. Tested against
mkj/dropbear at commit 99baf1828c13e88de048e641d03694bfe8923251
(2026-11-11), built from source, full history reviewed:

1. TCP forwarding enforces the per-destination restrictions. Local TCP
   (direct-tcpip) calls svr_pubkey_allows_local_tcpfwd(desthost, destport)
   at src/svr-tcpfwd.c:202, which walks the key's permitopen list
   (src/svr-authpubkeyoptions.c:98-118). Remote TCP calls
   svr_pubkey_allows_remote_tcpfwd() at src/svr-tcpfwd.c:99, which walks
   the permitlisten list (:123-145). A destination that is not listed is
   administratively prohibited.

2. Unix-socket forwarding — direct-streamlocal@...nssh.com and
   streamlocal-forward@...nssh.com — checks only the coarse flag. The
   local path (newstreamlocal, src/svr-streamfwd.c:294) tests
   svr_opts.nolocaltcp and svr_pubkey_allows_tcpfwd(); the remote path
   (svr_remotestreamlocalreq, src/svr-streamfwd.c:219) tests
   svr_opts.noremotefwd and the same function. svr_pubkey_allows_tcpfwd()
   (src/svr-authpubkeyoptions.c:65-72) knows only the no-port-forwarding
   flag. Neither streamlocal path calls either per-destination function,
   and a search of the tree confirms those two functions are referenced
   only from src/svr-tcpfwd.c — never from src/svr-streamfwd.c.

3. The socket paths are on by default: src/default_options.h:76-77 sets
   DROPBEAR_SVR_LOCALSTREAMFWD and DROPBEAR_SVR_REMOTESTREAMFWD to 1,
   and the channel is registered at src/svr-session.c:80.

4. This is an incomplete fix, in the vendor's own words. Commit 796a89ef
   (2024-04-03), "Apply tcp forwarding restrictions to unix sockets too",
   added the coarse gate to newstreamlocal() and updated the manpage to
   say no-port-forwarding covers unix streams. The per-destination
   restrictions — the other half of what that title announces — were not
   applied. The author notes the absence himself at
   src/svr-streamfwd.c:213-214: the check could be relaxed if a
   permitlisten equivalent were added for streamlocal. It has not been,
   on any branch, as of the tested commit. A sibling fix (00cfa132)
   applied a different per-key restriction to the same path, so the path
   is not one the author considers outside the restriction model.

Live proof, against our own rig only (server built from the pinned
source, localhost): a test account's only key carried
permitopen="127.0.0.1:1",no-pty, and a unix echo server listened at
/tmp/target.sock. The permitted TCP destination (127.0.0.1:1) passed the
permitopen check and failed only on connect — the restriction parsed and
active. The non-permitted TCP destination (127.0.0.1:22) was refused:
"Administratively prohibited" — permitopen enforced, same key, same
session. The streamlocal channel to /tmp/target.sock was accepted, and
returned the echo server's banner, UNIX-SOCKET-REACHED. The list was
never consulted. Replicated once from a clean build. Harness and raw log
accompany this paper.

The escalation was fired the same way. The test account was placed in
the docker group — the normal condition for the CI and deploy accounts
that receive restricted keys — and the same key opened a
direct-streamlocal channel to /var/run/docker.sock and issued a Docker
API request over it: HTTP/1.1 200 OK, Api-Version 1.56, the real daemon.
TCP to a non-permitted destination stayed prohibited in the same
session. The Docker socket is host-root-equivalent; the daemon will run
a container with the host filesystem mounted, as root.

Scope, stated plainly: this is not unauthenticated, and it is not a
10.0. The attacker holds a valid key — a key the administrator
deliberately restricted to one destination. Post-bypass reach is
bounded by the sockets that account's OS identity can touch: the Docker
socket, database sockets (/run/mysqld/mysqld.sock,
/run/postgresql/.s.PGSQL.5432), agent and service sockets, and, in the
permitlisten direction, the ability to make the server listen on an
arbitrary writable unix path and shadow a service socket. That bound is
not the boundary under discussion. permitopen exists for accounts that
are not trusted with general reach; for those accounts, streamlocal is
reach the administrator explicitly withheld. Scored as a range for
exactly that reason: 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) when the
account can reach the Docker daemon or a database socket, ~7 when it
can reach only its own sockets.

What held is credited at the same weight. The pre-authentication
surface was audited and exercised in the same pass and nothing is
claimed there: the post-quantum hybrid KEX bounds-checks every read and
asserts full input consumption, its length constants match the
reference implementations, strict-KEX handling is correct, and the
recent pre-auth-adjacent fixes are properly applied. The neighbouring
fixes in the forwarding family were likewise verified in place —
the ssh-askpass write, the long authorized_keys line split, the
curve25519 canonicality checks, the streamfwd drop-privileges error.
The vendor's own fuzz targets, built from the pinned source, ran clean
on seed inputs. This is a logic gap, the class a fuzzer cannot see:
the check exists, is correct, and is wired to only half the doors.

The fix is not complicated. Until a path-based equivalent exists, a key
carrying any per-destination restriction should be refused streamlocal
channels outright; the complete fix is a path-based option enforced in
svr-streamfwd.c with the same list walk the TCP path uses.

The restriction was applied to unix sockets in April 2024. The half of
it that names destinations is the half that was left behind.

Paper: https://archive.org/details/knell-not-an-address

Key: 133883AB9F6EDE5B9BA6B82E27BEDAF0CE152F25 (knell)

Verify: gpg --import knell-public.asc, then
gpg --verify paper-dropbear-FINAL-2026-11-13.md.asc

— knell

More, signed and timestamped:
https://github.com/the-knell

Sent from [Proton Mail](https://proton.me/mail/home) for Android.

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.