fix: v0.5.0 security and robustness fixes from the 2026-10-07 review
- Ask Claude: strip control characters, send as bracketed paste (closes #2) - end: forget before kill, so ending from inside a session leaves no ghost (closes #3) - reconnects use `attach --existing` and never resurrect ended/renamed sessions (closes #4) - repo: git --no-optional-locks, no safe.directory override, refresh on connect/bell/60s (closes #5) - uploads: unique names via mktemp, cleanup trap on hangup (closes #6) - clipboard: write-only OSC 52 provider, tmux allow-passthrough off (closes #7) - uploads: size check up front, streamed slices, idle timeout (closes #9) - --list: path last in TSV rows, tolerant numbers (closes #10) - tokens via curl config on stdin; userinfo stripped from remote URLs (closes #11) - ttyd max clients 32 and at most 2 concurrent side-channel calls (#8; BroadcastChannel polling still open) - tmux: fixed terminal-features index (no duplicates on re-source), no status-right clock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+43
-20
@@ -11,7 +11,7 @@
|
||||
# devdesk pick interactive picker
|
||||
#
|
||||
# The web UI calls `devdesk attach --list | --repo NAME | --alerts | --upload SESSION FILE BYTES |
|
||||
# --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
|
||||
# --new NAME [PROJECT] | --rename OLD NEW | --end NAME | --existing NAME`
|
||||
# through ttyd; those print JSON.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -25,7 +25,8 @@ usage() {
|
||||
# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
|
||||
running_tsv() {
|
||||
dd_server_up || return 0
|
||||
dd_tmux list-sessions -F '#{session_name} #{session_path} #{session_attached} #{session_activity} #{session_windows}' 2>/dev/null || true
|
||||
# Path last: it may contain anything (tabs included) and is re-joined below.
|
||||
dd_tmux list-sessions -F '#{session_name} #{session_attached} #{session_activity} #{session_windows} #{session_path}' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Tab-separated top-level project directories: slug, dir.
|
||||
@@ -68,9 +69,9 @@ cmd_list() {
|
||||
--arg root "$DEVDESK_PROJECTS_ROOT" '
|
||||
def rows($s): $s | split("\n") | map(select(length > 0) | split("\t"));
|
||||
($reg.sessions // {}) as $known
|
||||
| [rows($run)[] | {name: .[0], dir: .[1], running: true,
|
||||
attached: (.[2] | tonumber), activity: (.[3] | tonumber),
|
||||
windows: (.[4] | tonumber)}] as $live
|
||||
| [rows($run)[] | {name: .[0], dir: (.[4:] | join("\t")), running: true,
|
||||
attached: (.[1] | tonumber? // 0), activity: (.[2] | tonumber? // 0),
|
||||
windows: (.[3] | tonumber? // 0)}] as $live
|
||||
| ($live | map(.name)) as $liveNames
|
||||
| {default: $dflt, root: $root,
|
||||
sessions: (
|
||||
@@ -82,7 +83,7 @@ cmd_list() {
|
||||
claude_session_id: (.value.claude_session_id // null)}))
|
||||
) | map(. + {context: ($ctx[.name] // null), status: ($st[.name] // null)}),
|
||||
projects: ([{name: $dflt, dir: $root}]
|
||||
+ [rows($proj)[] | {name: .[0], dir: .[1]}
|
||||
+ [rows($proj)[] | {name: .[0], dir: (.[1:] | join("\t"))}
|
||||
| select(.name | test("^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"))])}'
|
||||
}
|
||||
|
||||
@@ -143,19 +144,23 @@ cmd_rename() {
|
||||
cmd_end() {
|
||||
[ $# -eq 1 ] || dd_die "usage: devdesk end NAME"
|
||||
dd_valid_name "$1" || dd_die "invalid session name: $1"
|
||||
# Forget first: killing the session SIGHUPs us when run from inside it.
|
||||
dd_registry_remove "$1"
|
||||
rm -rf "${DEVDESK_STATE_DIR:?}/uploads/$1"
|
||||
if dd_server_up; then
|
||||
dd_tmux kill-session -t "=$1" 2>/dev/null || true
|
||||
fi
|
||||
dd_registry_remove "$1"
|
||||
}
|
||||
|
||||
# GET $DEVDESK_GITEA_URL/api/v1<path>; token optional (private repos only).
|
||||
gitea_get() {
|
||||
local auth=()
|
||||
if [ -r "$DEVDESK_GITEA_TOKEN_FILE" ]; then
|
||||
auth=(-H "Authorization: token $(tr -d '[:space:]' <"$DEVDESK_GITEA_TOKEN_FILE")")
|
||||
fi
|
||||
curl -fsS --max-time 4 "${auth[@]}" "$DEVDESK_GITEA_URL/api/v1$1" 2>/dev/null
|
||||
# The token goes in via a curl config on stdin, never on the command line (ps).
|
||||
{
|
||||
if [ -r "$DEVDESK_GITEA_TOKEN_FILE" ]; then
|
||||
# shellcheck disable=SC1003 # tr deletes whitespace, quotes and backslashes from the token
|
||||
printf 'header = "Authorization: token %s"\n' "$(tr -d '[:space:]"\\' <"$DEVDESK_GITEA_TOKEN_FILE")"
|
||||
fi
|
||||
} | curl -fsS --max-time 4 -K - "$DEVDESK_GITEA_URL/api/v1$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# owner/repo when REMOTE points at our Gitea (https or ssh, any configured host), else nothing.
|
||||
@@ -177,7 +182,9 @@ cmd_repo() {
|
||||
dd_valid_name "$1" || dd_die "invalid session name: $1"
|
||||
local dir g branch sha="" dirty ahead=null behind=null upstream=false remote slug="" ci=null prs=null
|
||||
dir=$(dd_resolve_dir "$1")
|
||||
g=(git -c safe.directory='*' -C "$dir")
|
||||
# --no-optional-locks: never take .git/index.lock (Claude's own git would collide).
|
||||
# No safe.directory override: repos owned by other users stay untrusted.
|
||||
g=(git --no-optional-locks -C "$dir")
|
||||
if ! "${g[@]}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
jq -cMn --arg s "$1" --arg dir "$dir" '{session: $s, dir: $dir, git: false}'
|
||||
return
|
||||
@@ -189,7 +196,7 @@ cmd_repo() {
|
||||
upstream=true
|
||||
read -r behind ahead < <("${g[@]}" rev-list --left-right --count '@{u}...HEAD')
|
||||
fi
|
||||
remote=$("${g[@]}" remote get-url origin 2>/dev/null || true)
|
||||
remote=$("${g[@]}" remote get-url origin 2>/dev/null | sed -E 's#^([a-z+]+://)[^/@]*@#\1#' || true)
|
||||
[ -n "$remote" ] && slug=$(gitea_slug "$remote")
|
||||
if [ -n "$slug" ] && [ -n "$sha" ]; then
|
||||
ci=$(gitea_get "/repos/$slug/commits/$sha/status" |
|
||||
@@ -216,11 +223,13 @@ cmd_alerts() {
|
||||
jq -cMn '{configured: false, alerts: []}'
|
||||
return
|
||||
fi
|
||||
local auth=() raw
|
||||
if [ -r "$DEVDESK_GRAFANA_TOKEN_FILE" ]; then
|
||||
auth=(-H "Authorization: Bearer $(tr -d '[:space:]' <"$DEVDESK_GRAFANA_TOKEN_FILE")")
|
||||
fi
|
||||
if ! raw=$(curl -fsS --max-time 6 "${auth[@]}" "$DEVDESK_GRAFANA_URL/api/prometheus/grafana/api/v1/rules" 2>&1); then
|
||||
local raw
|
||||
if ! raw=$({
|
||||
if [ -r "$DEVDESK_GRAFANA_TOKEN_FILE" ]; then
|
||||
# shellcheck disable=SC1003 # tr deletes whitespace, quotes and backslashes from the token
|
||||
printf 'header = "Authorization: Bearer %s"\n' "$(tr -d '[:space:]"\\' <"$DEVDESK_GRAFANA_TOKEN_FILE")"
|
||||
fi
|
||||
} | curl -fsS --max-time 6 -K - "$DEVDESK_GRAFANA_URL/api/prometheus/grafana/api/v1/rules" 2>&1); then
|
||||
jq -cMn --arg e "${raw:-no response}" '{configured: true, error: $e, alerts: []}'
|
||||
return
|
||||
fi
|
||||
@@ -255,7 +264,10 @@ cmd_upload() {
|
||||
dir="$DEVDESK_STATE_DIR/uploads/$session"
|
||||
mkdir -p "$dir"
|
||||
find "$DEVDESK_STATE_DIR/uploads" -type f -mtime +7 -delete 2>/dev/null || true
|
||||
dest="$dir/$(date +%Y%m%d-%H%M%S)-$name"
|
||||
# Unique even for same-second uploads of the same name.
|
||||
dest=$(mktemp --tmpdir="$dir" --suffix="-$name" "$(date +%Y%m%d-%H%M%S)-XXXX")
|
||||
# A dropped connection SIGHUPs us mid-transfer: never leave a partial file.
|
||||
trap 'rm -f "$dest"' HUP INT TERM
|
||||
# shellcheck disable=SC2017 # integer ceil(size/3)*4: the base64 length, on purpose
|
||||
b64len=$(((size + 2) / 3 * 4))
|
||||
stty raw -echo 2>/dev/null || true
|
||||
@@ -267,6 +279,8 @@ cmd_upload() {
|
||||
rm -f "$dest"
|
||||
dd_die "upload incomplete ($actual of $size bytes)"
|
||||
fi
|
||||
trap - HUP INT TERM
|
||||
chmod 0644 "$dest"
|
||||
jq -cMn --arg p "$dest" '{ok: true, path: $p}'
|
||||
}
|
||||
|
||||
@@ -305,6 +319,15 @@ cmd_attach() {
|
||||
ok_json
|
||||
return
|
||||
;;
|
||||
--existing)
|
||||
# Reconnects: never re-create a session that was ended or renamed meanwhile.
|
||||
shift
|
||||
[ $# -eq 1 ] || dd_die "attach --existing takes exactly one session name"
|
||||
dd_valid_name "$1" || dd_die "invalid session name: $1"
|
||||
if ! { dd_server_up && dd_tmux has-session -t "=$1" 2>/dev/null; } && [ -z "$(dd_registry_get "$1" dir)" ]; then
|
||||
dd_die "session '$1' no longer exists (it was ended or renamed)"
|
||||
fi
|
||||
;;
|
||||
-*) dd_die "unknown option: $1" ;;
|
||||
esac
|
||||
[ $# -eq 1 ] || dd_die "attach takes exactly one session name"
|
||||
|
||||
Reference in New Issue
Block a user