diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c78475..5c652d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ + - If a variable's value meets certain entropy criteria and matches an environment variable value, + pdoc will now emit a warning and display the variable's name as a placeholder instead. + This heuristic is meant to prevent accidental leakage of environment secrets and can be disabled by setting + `PDOC_DISPLAY_ENV_VARS=1`. + ([#622](https://github.com/mitmproxy/pdoc/pull/622), @mhils) ## 2023-09-10: pdoc 14.1.0 diff --git a/pdoc/doc.py b/pdoc/doc.py index 1c74481f..2f00b1c8 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -1106,6 +1106,23 @@ def default_value_str(self) -> str: if self.default_value is empty: return "" + # This is not perfect, but a solid attempt at preventing accidental leakage of secrets. + # If you have input on how to improve the heuristic, please send a pull request! + value_taken_from_env_var = ( + isinstance(self.default_value, str) + and len(self.default_value) >= 8 + and self.default_value in _environ_lookup() + ) + if value_taken_from_env_var and not os.environ.get("PDOC_DISPLAY_ENV_VARS", ""): + env_var = "$" + _environ_lookup()[self.default_value] + warnings.warn( + f"The default value of {self.fullname} matches the {env_var} environment variable. " + f"To prevent accidental leakage of secrets, the default value is not displayed. " + f"Disable this behavior by setting PDOC_DISPLAY_ENV_VARS=1 as an environment variable.", + RuntimeWarning, + ) + return env_var + try: pretty = repr(self.default_value) except Exception as e: @@ -1124,6 +1141,14 @@ def annotation_str(self) -> str: return "" +@cache +def _environ_lookup(): + """ + A reverse lookup of os.environ. This is a cached function so that it is evaluated lazily. + """ + return {value: key for key, value in os.environ.items()} + + class _PrettySignature(inspect.Signature): """ A subclass of `inspect.Signature` that pads __str__ over several lines diff --git a/test/test_doc.py b/test/test_doc.py index 84d7186d..98208460 100644 --- a/test/test_doc.py +++ b/test/test_doc.py @@ -11,6 +11,7 @@ from pdoc.doc import Class from pdoc.doc import Module from pdoc.doc import Variable +from pdoc.doc import _environ_lookup from pdoc.doc_types import empty here = Path(__file__).parent @@ -143,3 +144,34 @@ def test_raising_submodules(): assert m.submodules finally: f.write_bytes(b"# syntax error will be inserted by test here\n") + + +def test_default_value_masks_env_vars(monkeypatch): + monkeypatch.setenv("SUPER_SECRET_TOKEN", "correct horse battery staple") + monkeypatch.setenv("VERSION_NUMBER", "42.0.1") + _environ_lookup.cache_clear() + try: + v1 = Variable( + "module", + "var", + taken_from=("module", "var"), + docstring="", + annotation=empty, + default_value="correct horse battery staple", + ) + with pytest.warns( + match=r"The default value of module.var matches the \$SUPER_SECRET_TOKEN environment variable." + ): + assert v1.default_value_str == "$SUPER_SECRET_TOKEN" + + v2 = Variable( + "module", + "version", + taken_from=("module", "version"), + docstring="", + annotation=empty, + default_value="42.0.1", + ) + assert v2.default_value_str == "'42.0.1'" + finally: + _environ_lookup.cache_clear()