diff --git a/.jules/sentinel.md b/.jules/sentinel.md index be2dfa4bb..08612ecb2 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -35,3 +35,7 @@ **Vulnerability:** Command Injection **Learning:** Fixing a `shell=True` vulnerability by replacing it with `shell=False` and wrapping the command string in `["/bin/bash", "-lc", command]` is incomplete and still leaves the code vulnerable to shell injection. It acts as security theater, as it misleads linters while executing untrusted input via the bash wrapper. The vulnerability was still present in `sandboxed_web_e2e.py`. **Prevention:** Remove `/bin/bash` wrapper from `subprocess` calls in CI scripts. Always use `shlex.split(command)` to safely parse strings into a list of arguments and pass the list directly to `subprocess.Popen` or `subprocess.run`. +## 2026-07-28 - Redact Sensitive Data from Sandboxed Execution Logs +**Vulnerability:** Information Disclosure / Secret Leakage +**Learning:** Subprocesses executed in sandbox environments can output sensitive tokens to their stdout, stderr, or log files. If these are printed directly to the console or CI logs, it can lead to credential leakage. +**Prevention:** Always use `redact_text` (with an `ImportError` fallback) to scrub sensitive tokens before printing subprocess outputs or reading log tails in CI scripts. diff --git a/scripts/ci/sandboxed_verify.py b/scripts/ci/sandboxed_verify.py index aace18d45..9d8da714c 100644 --- a/scripts/ci/sandboxed_verify.py +++ b/scripts/ci/sandboxed_verify.py @@ -14,6 +14,13 @@ from collections.abc import Sequence from pathlib import Path +try: + from scripts.ci.redact_sensitive_log import redact_text +except ImportError: # pragma: no cover + + def redact_text(text: str) -> str: + return text + DEFAULT_IGNORE = ( ".git", @@ -219,17 +226,17 @@ def main(argv: Sequence[str] | None = None) -> int: try: completed = run_command(args.command, copied_repo, env, args.timeout) if completed.stdout: - print(completed.stdout, end="") + print(redact_text(completed.stdout), end="") if completed.stderr: - print(completed.stderr, end="", file=sys.stderr) + print(redact_text(completed.stderr), end="", file=sys.stderr) exit_code = completed.returncode except subprocess.TimeoutExpired as exc: stdout = timeout_output_text(exc.stdout) stderr = timeout_output_text(exc.stderr) if stdout: - print(stdout, end="" if stdout.endswith("\n") else "\n") + print(redact_text(stdout), end="" if stdout.endswith("\n") else "\n") if stderr: - print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr) + print(redact_text(stderr), end="" if stderr.endswith("\n") else "\n", file=sys.stderr) print(f"sandboxed-verify: command timed out after {args.timeout}s", file=sys.stderr) exit_code = 124 return exit_code diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index ae0c3105a..cdeea06c9 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -23,6 +23,13 @@ from scripts.ci import sandboxed_verify +try: + from scripts.ci.redact_sensitive_log import redact_text +except ImportError: # pragma: no cover + + def redact_text(text: str) -> str: + return text + RESULT_MARKER = "SANDBOXED_WEB_E2E_RESULT" @@ -232,18 +239,18 @@ def main(argv: Sequence[str] | None = None) -> int: try: completed = run_shell(args.e2e_cmd, copied_repo, env, args.e2e_timeout) if completed.stdout: - print(completed.stdout, end="") + print(redact_text(completed.stdout), end="") if completed.stderr: - print(completed.stderr, end="", file=sys.stderr) + print(redact_text(completed.stderr), end="", file=sys.stderr) exit_code = completed.returncode return exit_code except subprocess.TimeoutExpired as exc: stdout = sandboxed_verify.timeout_output_text(exc.stdout) stderr = sandboxed_verify.timeout_output_text(exc.stderr) if stdout: - print(stdout, end="" if stdout.endswith("\n") else "\n") + print(redact_text(stdout), end="" if stdout.endswith("\n") else "\n") if stderr: - print(stderr, end="" if stderr.endswith("\n") else "\n", file=sys.stderr) + print(redact_text(stderr), end="" if stderr.endswith("\n") else "\n", file=sys.stderr) print(f"sandboxed-web-e2e: e2e command timed out after {args.e2e_timeout}s", file=sys.stderr) exit_code = 124 return exit_code @@ -253,7 +260,7 @@ def main(argv: Sequence[str] | None = None) -> int: log_tail = tail_text(service.log_path) if log_tail: print(f"--- {service.label} log tail ---") - print(log_tail) + print(redact_text(log_tail)) emit_result( args=args, copied_repo=copied_repo,