diff --git a/CHANGES.rst b/CHANGES.rst index 5a37e43db..ec7142195 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,7 +28,12 @@ Unreleased as invisible functions and coverage.py would warn you if they weren't completely executed. This no longer happens under Python 3.12. +- Fix: the ``coverage debug sys`` command includes some environment variables + in its output. This could have included sensitive data. Those values are + now hidden with asterisks, closing `issue 1628`_. + .. _issue 1553: https://github.com/nedbat/coveragepy/issues/1553 +.. _issue 1628: https://github.com/nedbat/coveragepy/issues/1628 .. scriv-start-here diff --git a/coverage/control.py b/coverage/control.py index 2a84ce710..723c4d876 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -29,7 +29,9 @@ from coverage.config import CoverageConfig, read_coverage_config from coverage.context import should_start_context_test_function, combine_context_switchers from coverage.data import CoverageData, combine_parallel_data -from coverage.debug import DebugControl, NoDebugging, short_stack, write_formatted_info +from coverage.debug import ( + DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display +) from coverage.disposition import disposition_debug_msg from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory @@ -37,7 +39,7 @@ from coverage.inorout import InOrOut from coverage.jsonreport import JsonReporter from coverage.lcovreport import LcovReporter -from coverage.misc import bool_or_none, join_regex, human_sorted +from coverage.misc import bool_or_none, join_regex from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module from coverage.multiproc import patch_multiprocessing from coverage.plugin import FileReporter @@ -1298,14 +1300,7 @@ def plugin_info(plugins: List[Any]) -> List[str]: ("pid", os.getpid()), ("cwd", os.getcwd()), ("path", sys.path), - ("environment", human_sorted( - f"{k} = {v}" - for k, v in os.environ.items() - if ( - any(slug in k for slug in ("COV", "PY")) or - (k in ("HOME", "TEMP", "TMP")) - ) - )), + ("environment", [f"{k} = {v}" for k, v in relevant_environment_display(os.environ)]), ("command_line", " ".join(getattr(sys, "argv", ["-none-"]))), ] diff --git a/coverage/debug.py b/coverage/debug.py index 3ef6dae8a..3484792e2 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -12,16 +12,18 @@ import itertools import os import pprint +import re import reprlib import sys import types import _thread from typing import ( - Any, Callable, IO, Iterable, Iterator, Optional, List, Tuple, cast, + cast, + Any, Callable, IO, Iterable, Iterator, Mapping, Optional, List, Tuple, ) -from coverage.misc import isolate_module +from coverage.misc import human_sorted_items, isolate_module from coverage.types import TWritable os = isolate_module(os) @@ -489,3 +491,34 @@ def _clean_stack_line(s: str) -> str: # pragma: debugging s = s.replace(os.path.dirname(os.__file__) + "/", "") s = s.replace(sys.prefix + "/", "") return s + + +def relevant_environment_display(env: Mapping[str, str]) -> List[Tuple[str, str]]: + """Filter environment variables for a debug display. + + Select variables to display (with COV or PY in the name, or HOME, TEMP, or + TMP), and also cloak sensitive values with asterisks. + + Arguments: + env: a dict of environment variable names and values. + + Returns: + A list of pairs (name, value) to show. + + """ + slugs = {"COV", "PY"} + include = {"HOME", "TEMP", "TMP"} + cloak = {"API", "TOKEN", "KEY", "SECRET", "PASS", "SIGNATURE"} + + to_show = [] + for name, val in env.items(): + keep = False + if name in include: + keep = True + elif any(slug in name for slug in slugs): + keep = True + if keep: + if any(slug in name for slug in cloak): + val = re.sub(r"\w", "*", val) + to_show.append((name, val)) + return human_sorted_items(to_show) diff --git a/tests/test_debug.py b/tests/test_debug.py index 60a7b10a4..e611134d0 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -19,7 +19,8 @@ from coverage import env from coverage.debug import ( DebugOutputFile, - clipped_repr, filter_text, info_formatter, info_header, short_id, short_stack, + clipped_repr, filter_text, info_formatter, info_header, relevant_environment_display, + short_id, short_stack, ) from tests.coveragetest import CoverageTest @@ -297,3 +298,22 @@ def test_short_stack_limit(self) -> None: def test_short_stack_skip(self) -> None: stack = f_one(skip=1).splitlines() assert "f_two" in stack[-1] + + +def test_relevant_environment_display() -> None: + env_vars = { + "HOME": "my home", + "HOME_DIR": "other place", + "XYZ_NEVER_MIND": "doesn't matter", + "SOME_PYOTHER": "xyz123", + "COVERAGE_THING": "abcd", + "MY_PYPI_TOKEN": "secret.something", + "TMP": "temporary", + } + assert relevant_environment_display(env_vars) == [ + ("COVERAGE_THING", "abcd"), + ("HOME", "my home"), + ("MY_PYPI_TOKEN", "******.*********"), + ("SOME_PYOTHER", "xyz123"), + ("TMP", "temporary"), + ]