From b69b1eff83b62cd1281ad8e31c29249d9fdc1cea Mon Sep 17 00:00:00 2001 From: Rome-1 Date: Thu, 1 Oct 2026 16:38:34 -0700 Subject: [PATCH] fix(python): do not render local variables in tracebacks Typer's pretty tracebacks print every frame's locals by default. Request helpers keep credentials in locals, so an unhandled exception could echo them to stderr. Turn show_locals off on the root app. Adds a subprocess test that forces a transport failure and asserts a sentinel credential never reaches stdout or stderr. --- python/rafter_cli/__main__.py | 2 ++ python/tests/test_traceback_no_locals.py | 45 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 python/tests/test_traceback_no_locals.py diff --git a/python/rafter_cli/__main__.py b/python/rafter_cli/__main__.py index fc1ae807..ed79863c 100644 --- a/python/rafter_cli/__main__.py +++ b/python/rafter_cli/__main__.py @@ -25,6 +25,8 @@ help="Rafter CLI — the default security agent for AI workflows. Free for individuals and open source. No account required.", add_completion=True, no_args_is_help=True, + # Never render frame locals in tracebacks: they hold the API key. + pretty_exceptions_show_locals=False, ) diff --git a/python/tests/test_traceback_no_locals.py b/python/tests/test_traceback_no_locals.py new file mode 100644 index 00000000..43255845 --- /dev/null +++ b/python/tests/test_traceback_no_locals.py @@ -0,0 +1,45 @@ +"""An unhandled exception must not print the API key. + +Typer's pretty tracebacks render every frame's local variables by default, +and the request helpers hold the key in ``api_key`` / ``headers`` locals. +A plain transport failure (here: an unreachable proxy) is enough to raise +out of a command, so the key would land on stderr, which in CI is the +build log. +""" +from __future__ import annotations + +import os +import subprocess +import sys + +SENTINEL = "RAFTER_TEST_SENTINEL_KEY_9f3c1a" + + +def test_unhandled_exception_does_not_print_api_key(tmp_path): + env = os.environ.copy() + env.pop("_TYPER_STANDARD_TRACEBACK", None) + env.update( + { + "HOME": str(tmp_path), + "RAFTER_API_KEY": SENTINEL, + "HTTPS_PROXY": "http://127.0.0.1:1", + "https_proxy": "http://127.0.0.1:1", + "NO_PROXY": "", + "no_proxy": "", + } + ) + result = subprocess.run( + [sys.executable, "-m", "rafter_cli", "usage"], + capture_output=True, + text=True, + cwd=tmp_path, + env=env, + timeout=60, + ) + + assert result.returncode != 0 + # Positive control: the command did fail with a traceback, so the + # assertion below is about its content rather than its absence. + assert "Traceback" in result.stderr or "Error" in result.stderr + assert SENTINEL not in result.stderr + assert SENTINEL not in result.stdout