-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Hide security-sensitive strings in VCS command log messages #6890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ae70014
3842e85
5ffc930
98e7ad7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Hide security-sensitive strings like passwords in log messages related to | ||
version control system (aka VCS) command invocations. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -61,6 +61,7 @@ | |
from pip._internal.utils.ui import SpinnerInterface | ||
|
||
VersionInfo = Tuple[int, int, int] | ||
CommandArgs = List[Union[str, 'HiddenText']] | ||
else: | ||
# typing's cast() is needed at runtime, but we don't want to import typing. | ||
# Thus, we use a dummy no-op version, which we tell mypy to ignore. | ||
|
@@ -749,8 +750,8 @@ def unpack_file( | |
is_svn_page(file_contents(filename))): | ||
# We don't really care about this | ||
from pip._internal.vcs.subversion import Subversion | ||
url = 'svn+' + link.url | ||
Subversion().unpack(location, url=url) | ||
hidden_url = hide_url('svn+' + link.url) | ||
Subversion().unpack(location, url=hidden_url) | ||
else: | ||
# FIXME: handle? | ||
# FIXME: magic signatures? | ||
|
@@ -764,16 +765,52 @@ def unpack_file( | |
) | ||
|
||
|
||
def make_command(*args): | ||
# type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs | ||
""" | ||
Create a CommandArgs object. | ||
""" | ||
command_args = [] # type: CommandArgs | ||
for arg in args: | ||
# Check for list instead of CommandArgs since CommandArgs is | ||
# only known during type-checking. | ||
if isinstance(arg, list): | ||
command_args.extend(arg) | ||
else: | ||
# Otherwise, arg is str or HiddenText. | ||
command_args.append(arg) | ||
|
||
return command_args | ||
|
||
|
||
def format_command_args(args): | ||
# type: (List[str]) -> str | ||
# type: (Union[List[str], CommandArgs]) -> str | ||
""" | ||
Format command arguments for display. | ||
""" | ||
return ' '.join(shlex_quote(arg) for arg in args) | ||
# For HiddenText arguments, display the redacted form by calling str(). | ||
# Also, we don't apply str() to arguments that aren't HiddenText since | ||
# this can trigger a UnicodeDecodeError in Python 2 if the argument | ||
# has type unicode and includes a non-ascii character. (The type | ||
# checker doesn't ensure the annotations are correct in all cases.) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reg the type-checker: I think we're supposed to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is supposed to always be |
||
return ' '.join( | ||
shlex_quote(str(arg)) if isinstance(arg, HiddenText) | ||
else shlex_quote(arg) for arg in args | ||
) | ||
|
||
|
||
def reveal_command_args(args): | ||
# type: (Union[List[str], CommandArgs]) -> List[str] | ||
""" | ||
Return the arguments in their raw, unredacted form. | ||
""" | ||
return [ | ||
arg.secret if isinstance(arg, HiddenText) else arg for arg in args | ||
] | ||
|
||
|
||
def make_subprocess_output_error( | ||
cmd_args, # type: List[str] | ||
cmd_args, # type: Union[List[str], CommandArgs] | ||
cwd, # type: Optional[str] | ||
lines, # type: List[Text] | ||
exit_status, # type: int | ||
|
@@ -815,7 +852,7 @@ def make_subprocess_output_error( | |
|
||
|
||
def call_subprocess( | ||
cmd, # type: List[str] | ||
cmd, # type: Union[List[str], CommandArgs] | ||
show_stdout=False, # type: bool | ||
cwd=None, # type: Optional[str] | ||
on_returncode='raise', # type: str | ||
|
@@ -882,7 +919,9 @@ def call_subprocess( | |
env.pop(name, None) | ||
try: | ||
proc = subprocess.Popen( | ||
cmd, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, | ||
# Convert HiddenText objects to the underlying str. | ||
reveal_command_args(cmd), | ||
stderr=subprocess.STDOUT, stdin=subprocess.PIPE, | ||
stdout=subprocess.PIPE, cwd=cwd, env=env, | ||
) | ||
proc.stdin.close() | ||
|
@@ -1199,6 +1238,52 @@ def redact_password_from_url(url): | |
return _transform_url(url, _redact_netloc)[0] | ||
|
||
|
||
class HiddenText(object): | ||
def __init__( | ||
self, | ||
secret, # type: str | ||
redacted, # type: str | ||
): | ||
# type: (...) -> None | ||
self.secret = secret | ||
self.redacted = redacted | ||
|
||
def __repr__(self): | ||
# type: (...) -> str | ||
return '<HiddenText {!r}>'.format(str(self)) | ||
|
||
def __str__(self): | ||
# type: (...) -> str | ||
return self.redacted | ||
|
||
# This is useful for testing. | ||
def __eq__(self, other): | ||
# type: (Any) -> bool | ||
if type(self) != type(other): | ||
return False | ||
|
||
# The string being used for redaction doesn't also have to match, | ||
# just the raw, original string. | ||
return (self.secret == other.secret) | ||
|
||
# We need to provide an explicit __ne__ implementation for Python 2. | ||
# TODO: remove this when we drop PY2 support. | ||
def __ne__(self, other): | ||
# type: (Any) -> bool | ||
return not self == other | ||
|
||
|
||
def hide_value(value): | ||
# type: (str) -> HiddenText | ||
return HiddenText(value, redacted='****') | ||
|
||
|
||
def hide_url(url): | ||
# type: (str) -> HiddenText | ||
redacted = redact_password_from_url(url) | ||
return HiddenText(url, redacted=redacted) | ||
|
||
|
||
def protect_pip_from_modification_on_windows(modifying_pip): | ||
"""Protection of pip.exe from modification on Windows | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.