From b9a50eabc4ce7666ebb2038dd21601998fe5cc55 Mon Sep 17 00:00:00 2001 From: Jack Platten Date: Thu, 16 Jul 2026 14:50:14 -0700 Subject: [PATCH] fix(vcs): scrub control chars from jj/pwd output (#65) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - jj commit descriptions and bookmark/tag names are freeform text (unlike git refnames, which forbid control characters), so a crafted description or bookmark in a repo you merely `cd` into could inject raw terminal escape sequences (title rewrite, cursor manipulation, clipboard tricks) since they previously flowed straight through to `echo -ns`. - Directory names can also legally contain control/ESC bytes on Unix, so `$PWD` components get the same treatment. - Both fixes are a single `string replace -ra` pass over the already-split data (`$raw_lines` in the jj backend, `$split_pwd` in `_tide_pwd`) — no forks, negligible cost. The jj fix preserves `\t` since that's the template's field separator; the pwd fix has no such exception. ## Test plan - [x] `mise run all` (fmt, lint, install, full littlecheck suite) — all green - [x] New regression case in `tests/_tide_item_vcs.test.fish`: a mocked `jj` emits a description containing a raw ESC byte; asserts the rendered item shows the text with the byte stripped. - [x] New regression case in `tests/_tide_item_pwd.test.fish`: a directory literally named with an embedded ESC byte; asserts `_tide_pwd` strips it. - [x] Verified in isolation (outside the test suite) that the regex actually strips the byte in both cases, confirming the new tests exercise real behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- functions/_tide_internal_vcs_jj.fish | 6 ++++++ functions/_tide_pwd.fish | 4 ++++ tests/_tide_item_pwd.test.fish | 4 ++++ tests/_tide_item_vcs.test.fish | 9 +++++++++ 4 files changed, 23 insertions(+) diff --git a/functions/_tide_internal_vcs_jj.fish b/functions/_tide_internal_vcs_jj.fish index 213e8f5..8ed42b3 100644 --- a/functions/_tide_internal_vcs_jj.fish +++ b/functions/_tide_internal_vcs_jj.fish @@ -48,6 +48,12 @@ if(self.contained_in("::trunk() & ~::@"), -T $tmpl 2>/dev/null) or return 1 + # Descriptions/bookmarks/tags are freeform text (unlike git refnames), + # so scrub control/ESC bytes before they ever reach the terminal -- a + # crafted commit description could otherwise inject escape sequences. + # \t is preserved: it's this template's field separator. + set raw_lines (string replace -ra '[\x00-\x08\x0b-\x1f\x7f]' '' -- $raw_lines) + # Colors # if bg color is normal, we can use matching jj colors if test $tide_jj_bg_color = normal diff --git a/functions/_tide_pwd.fish b/functions/_tide_pwd.fish index 0ac2c50..fe7b51f 100644 --- a/functions/_tide_pwd.fish +++ b/functions/_tide_pwd.fish @@ -8,6 +8,10 @@ set -l pwd_icon $tide_pwd_icon' ' eval "function _tide_pwd if set -l split_pwd (string replace -r '^$HOME' '~' -- \$PWD | string split /) + # Directory names may legally contain control/ESC bytes; scrub them + # before they reach the terminal (no \t exception here -- unlike the + # jj item, nothing downstream splits on tabs). + set split_pwd (string replace -ra '[\\x00-\\x1f\\x7f]' '' -- \$split_pwd) test -w . && set -f split_output \"$pwd_icon\$split_pwd[1]\" \$split_pwd[2..] || set -f split_output \"$unwritable_icon\$split_pwd[1]\" \$split_pwd[2..] set split_output[-1] \"$color_anchors\$split_output[-1]$reset_to_color_dirs\" diff --git a/tests/_tide_item_pwd.test.fish b/tests/_tide_item_pwd.test.fish index cff454b..b49cbc2 100644 --- a/tests/_tide_item_pwd.test.fish +++ b/tests/_tide_item_pwd.test.fish @@ -141,5 +141,9 @@ _pwd $tmpdir/tmp/noread/$longDir # CHECK: ~/t/n/a/b/c/d/e/f/golf/hotel/india/jul chmod 700 $tmpdir/tmp/noread command rm -r $tmpdir/tmp/noread +# ---------- Control/ESC bytes in a directory name are scrubbed ---------- +mkdir -p (printf '%s/tmp/evil\033dir/foo' $tmpdir) +_pwd (printf '%s/tmp/evil\033dir/foo' $tmpdir) # CHECK: ~/tmp/evildir/foo + # ------------------------------------Cleanup------------------------------------ command rm -r $tmpdir diff --git a/tests/_tide_item_vcs.test.fish b/tests/_tide_item_vcs.test.fish index 04e176d..8c20bc1 100644 --- a/tests/_tide_item_vcs.test.fish +++ b/tests/_tide_item_vcs.test.fish @@ -119,5 +119,14 @@ _vcs_item # CHECK: (@ abc123 main def456 * ↑1) touch .disable-jj-prompt _vcs_item # CHECK: +# -------- jj description control-char scrubbing -------- +# Regression test: a crafted commit description could otherwise inject a raw +# ESC byte straight into the terminal (see _tide_internal_vcs_jj.fish). +mkdir -p $dir/jj-scrub-repo/.jj +printf '#!/bin/sh\ncmd="$*"\ncase "$cmd" in\n *"workspace list"*) printf "default\\n" ;;\n *"log"*) printf "abc123\\t.\\tbookmark/main\\tdefault\\tdef456\\t*\\tfalse\\tevil\\033[31mdesc\\n" ;;\n *) exit 0 ;;\nesac\n' >$dir/bin/jj +cd $dir/jj-scrub-repo +set -e tide_jj_show_description +_vcs_item # CHECK: (@ abc123 main def456 * evil[31mdesc ↑1) + # ------ cleanup ------ command rm -r $dir -- knotmirror 0.0.1