From 3340c7d1bda064fae1c9d834a8054301d042d6f7 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Tue, 7 Jul 2026 12:30:30 +0200 Subject: [PATCH] job7 step B: redact printenv/env dumps in rtk-rewrite.sh (GITEA leak vector) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any single-pipeline printenv/env dump now gets a redaction pipe appended before it can reach stdout/transcript; `env VAR=x cmd` (legitimate subprocess launch) is left intact. Compound commands (;, &, ||) bail untouched — appending the pipe at the end would attach to the wrong segment. Discovered mid-implementation: rtk rewrite classifies any command containing "env" as exit-code 2 ("deny"), with no settings.json rule backing it — the command still reaches native evaluation and can run. Adjusted case 2/1 handling so the redaction check runs regardless. --- .claude/tasks/TODO.md | 54 +++++++++++++++++++++++++++++++++++ hooks/.rtk-hook.sha256 | 2 +- hooks/rtk-rewrite.sh | 45 +++++++++++++++++++++++------ lib/tests/rtk-rewrite.test.sh | 45 +++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 lib/tests/rtk-rewrite.test.sh diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 13da483..9eef3e8 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -1,5 +1,59 @@ # TODO +## 2026-07-07 — job7 secrets: triage backstops (chore/job7-secrets) +Genèse : `.audit/job7/ALL-REDACTED.json` (triage secrets multi-repo + ~/.claude). +GITEA_TOKEN déjà rotaté (transcript 960bd2cf). MAGIC rotation prévue après (A). +Fixtures git-game #5/#6 confirmées synthétiques (test-secret-*). Règle : jamais +manipuler une valeur de secret — edits sur les mécanismes seulement. + +- [x] A.1 Provenance MAGIC_API_KEY dans `~/.claude.json` : confirmée — + seul writer = `lib/toggle-external.sh:191` (`claude mcp add magic --scope + user --env API_KEY="$MAGIC_API_KEY"`), appelé par `install-plugins.sh` + (jamais un `claude mcp add` direct). Aucun autre writer (grep repo-wide). +- [x] A.2 Doc Claude Code (agent claude-code-guide) : `${VAR}` supporté dans + `env`/`command`/`args`/`url`/`headers` de mcpServers, y compris scope + user (`~/.claude.json`). Pas de `envFile`, pas de flag `mcp add` pour une + référence — édition manuelle requise. Voie SUPPORTÉE retenue. + Décision utilisateur : wiring `MAGIC_API_KEY` → wrapper `claude()` scopé + dans `~/.bashrc` (source `.env` en subshell, jamais exporté globalement) + plutôt qu'un export global (surface minimale, cohérent BDR-026). + - [ ] `~/.bashrc` : fonction `claude()` wrapper (subshell source ~/.claude/.env) + - [ ] `~/.claude.json` mcpServers.magic.env.API_KEY → `"${MAGIC_API_KEY}"` + (diff keys-only montré avant écriture) + - [ ] `lib/toggle-external.sh:191-192` — `--env API_KEY='${MAGIC_API_KEY}'` + (référence littérale, pas expansion bash) pour que les futurs + `enable magic` écrivent aussi la forme référence + - [ ] Doc README : procédure "ajouter un MCP avec secret" + piège `--env` + - [ ] Vérif manuelle : `claude mcp list` / relancer un MCP magic réel si possible +- [ ] A.3 Scrub one-shot des 5 backups `.claude.json.backup.*` existants + (prefix 78af0e36 hors `.env`) — script ou sed ciblé, vérif grep 0 hors .env +- [ ] A.4 Signaler à l'utilisateur : rotation MAGIC maintenant (après commit A) +- [x] B. Redaction dumps d'env — `hooks/rtk-rewrite.sh` étendu : pipeline simple + (pas de `;`/`&`/`||`) + `printenv`/`env` en tête sans `VAR=... cmd` derrière + → append `| sed -E 's/^([A-Za-z_]*(TOKEN|API_KEY|SECRET|PASSWORD|PASSWD) + [A-Za-z_]*)=.*/\1=REDACTED/'`. `env VAR=x cmd` intact. Compound bail + (`;`/`&`/`||`) — jamais de pipe attaché au mauvais segment. + - [x] `lib/tests/rtk-rewrite.test.sh` — 3 cas + garde compound + - [ ] `make test` vert +- [ ] C. Backstop gitleaks (8.30.1 confirmé installé — `protect` non listé, + `gitleaks git --staged` = sous-commande documentée retenue) + - [ ] `.gitleaks.toml` racine — allowlist 3 classes (vérifiées empiriquement + contre les vrais fichiers : marketplace.json sha 40-hex, ws-protocol + nonce, test-secret-[0-9-]+) + - [ ] pre-commit gitflow (`lib/gitflow.sh` `_gitflow_emit_pre_commit`) — + `gitleaks git --staged` après guard root/merge, non-bloquant si absent + - [ ] `lib/gitflow-test.sh` — faux secret staged bloqué ; PATH sans gitleaks + → warn + pass + - [ ] `make scan-secrets` — git × repos + dir ~/.claude, sortie → `.audit/` +- [ ] D. Purge (GO explicite par item) + - [ ] transcript 960bd2cf…jsonl — propose rm, attend GO + - [ ] `ide/27929.lock` stale — rm direct + - [ ] `cleanupPeriodDays` — vérifier nom exact champ doc, proposer 7j, diff + settings avant écriture +- [ ] Gate final : `make test` + `make scan-secrets` propre + table + étape/commit/gate + capitalize (BDR secrets-par-référence, MAJ BDR-026, + LRN piège `claude mcp add --env`) + ## 2026-07-05 — /deploy UX patch (feature/deploy-next-style) Feedback user au 1er run réel (bchanot-cv, [[EVAL-016]]) : NEXT.sh une commande par ligne (style session — ssh ouvre la box, la suite s'exécute dessus, local = diff --git a/hooks/.rtk-hook.sha256 b/hooks/.rtk-hook.sha256 index f908504..bdcd97e 100644 --- a/hooks/.rtk-hook.sha256 +++ b/hooks/.rtk-hook.sha256 @@ -1 +1 @@ -871efa28daf7c06a9c9039a2875407e2536646f5d82f7e7a9c6a80dd3742929c rtk-rewrite.sh +82369e32905a8de6dc6b2566c5992f686794a826b2310b15a96e4bd9d25ac7b6 rtk-rewrite.sh diff --git a/hooks/rtk-rewrite.sh b/hooks/rtk-rewrite.sh index 21dc98e..6242c74 100755 --- a/hooks/rtk-rewrite.sh +++ b/hooks/rtk-rewrite.sh @@ -18,9 +18,15 @@ # bypassed settings.json deny/ask). The REWRITTEN command goes # through native evaluation; explicit `rtk ` allow rules # in settings.json keep read-only forms frictionless. -# 1 No RTK equivalent → pass through unchanged +# 1 No RTK equivalent → command continues unchanged into the +# redaction check below (still may be rewritten there) # 2 Deny rule matched → pass through (Claude Code native deny handles it) # 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user +# +# Independent of the above: any command whose FINAL form is a single-pipeline +# `printenv`/`env` dump gets a redaction pipe appended (job7 — see below). +# This is a security post-process, not a token-savings rewrite, so it lives +# here rather than in the Rust registry. if ! command -v jq &>/dev/null; then echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2 @@ -71,17 +77,22 @@ EXIT_CODE=$? case $EXIT_CODE in 0) - # Rewrite found. If the output is identical, the command was - # already using RTK — nothing to do. - [ "$CMD" = "$REWRITTEN" ] && exit 0 + # Rewrite found. If identical to the input, RTK had nothing to add — + # keep going so the redaction check below still runs on it. + [ "$CMD" = "$REWRITTEN" ] && REWRITTEN="$CMD" ;; 1) - # No RTK equivalent — pass through unchanged. - exit 0 + # No RTK equivalent — keep the original command so the redaction + # check below still runs on it. + REWRITTEN="$CMD" ;; 2) - # Deny rule matched — let Claude Code's native deny rule handle it. - exit 0 + # Deny rule matched (rtk's own registry — not necessarily backed by a + # matching settings.json deny rule, so the original command can still + # reach native evaluation and run: e.g. bare `env`/`printenv` hits this + # exit code with no settings.json rule behind it). Keep the original + # command so the redaction check below still runs on it. + REWRITTEN="$CMD" ;; 3) # Ask rule matched — rewrite the command but do NOT auto-allow so that @@ -92,6 +103,24 @@ case $EXIT_CODE in ;; esac +# Security: redact raw environment dumps before they can reach stdout/the +# transcript (job7 — a bare `printenv`/`env` dump was the GITEA leak vector). +# `env VAR=x cmd` (env launching a subprocess with a var set) is legitimate +# and left intact. Scope: single-pipeline commands only — a command +# containing `;`, `&`, or `||` bails untouched, same "lose the feature +# rather than emit something wrong" rule as the RTK_ON_PATH substitution +# below: appending the redaction pipe at the end would silently attach to +# the WRONG segment of a compound command. +if ! printf '%s' "$REWRITTEN" | grep -Eq '[;&]' \ + && ! printf '%s' "$REWRITTEN" | grep -qF '||'; then + if printf '%s' "$REWRITTEN" | grep -Eq '^[[:space:]]*(printenv|env)([[:space:]]|$)' \ + && ! printf '%s' "$REWRITTEN" | grep -Eq '^[[:space:]]*env([[:space:]]+[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*)+[[:space:]]+[^|[:space:]]'; then + REWRITTEN="${REWRITTEN} | sed -E 's/^([A-Za-z_]*(TOKEN|API_KEY|SECRET|PASSWORD|PASSWD)[A-Za-z_]*)=.*/\1=REDACTED/'" + fi +fi + +[ "$CMD" = "$REWRITTEN" ] && exit 0 + # When rtk is NOT on PATH, a bare `rtk …` rewrite exits 127 in the tool # shell (whose PATH the hook cannot fix). Substitute the absolute path at # the string head — the only position safe to rewrite. Compound commands diff --git a/lib/tests/rtk-rewrite.test.sh b/lib/tests/rtk-rewrite.test.sh new file mode 100644 index 0000000..66f34bd --- /dev/null +++ b/lib/tests/rtk-rewrite.test.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# lib/tests/rtk-rewrite.test.sh +# job7 — printenv/env dump redaction pass in hooks/rtk-rewrite.sh. +set -u +H="$(cd "$(dirname "$0")/../.." && pwd)/hooks/rtk-rewrite.sh" +pass=0; fail=0 +check() { if [ "$2" = "$3" ]; then pass=$((pass+1)); else fail=$((fail+1)); + printf 'FAIL %s: got[%s] want[%s]\n' "$1" "$2" "$3"; fi; } + +# raw(cmd) -> the hook's stdout for a simulated PreToolUse Bash command. +raw() { + local input + input=$(jq -n --arg cmd "$1" '{tool_input:{command:$cmd}}') + printf '%s' "$input" | bash "$H" +} + +# fire(cmd) -> "redacted" if the hook appended the sed redaction pipe, +# "intact" if the command comes back unchanged/untouched. +fire() { + if raw "$1" | grep -q 'sed -E'; then echo redacted; else echo intact; fi +} + +# --- Env/printenv dumps must be redacted --- +check T1-bare-printenv "$(fire 'printenv')" redacted +check T2-bare-env "$(fire 'env')" redacted +check T3-env-pipe-grep "$(fire 'env | grep FOO')" redacted + +# --- `env VAR=x cmd` launches a subprocess — legitimate, left intact --- +check T4-env-legit "$(fire 'env FOO=bar cmd')" intact +check T5-env-legit-2vars "$(fire 'env A=1 B=2 cmd')" intact + +# --- Compound commands bail untouched (never attach the pipe to the wrong +# segment) --- +check T6-bail-and "$(fire 'env && true')" intact +check T7-bail-semi "$(fire 'env; true')" intact +check T8-bail-or "$(fire 'env || true')" intact + +# --- Regression: unrelated rtk-eligible commands still rewrite, untouched +# by the redaction pass --- +check T9-unrelated-still-rewrites \ + "$(raw 'cat /etc/hostname' | grep -c 'rtk ')" "1" +check T10-unrelated-not-redacted \ + "$(raw 'cat /etc/hostname' | grep -c 'sed -E')" "0" + +printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]