Skip to content

Commit

Permalink
add some safeguards to prevent accidental leakage of environment secr…
Browse files Browse the repository at this point in the history
…ets (#622)
  • Loading branch information
mhils authored Sep 20, 2023
1 parent c07d97c commit fdde9a8
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 0 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

<!-- ✨ You do not need to add a pull request reference or an author, this will be added automatically by CI. ✨ -->

- 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

Expand Down
25 changes: 25 additions & 0 deletions pdoc/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions test/test_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

0 comments on commit fdde9a8

Please sign in to comment.