[3.14] gh-138514: getpass: restrict echo_char to a single ASCII character (GH-138591) (#138988)

Co-authored-by: Benjamin Johnson <benjohnson2040@gmail.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Brian Schubert <brianm.schubert@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-09-17 16:20:45 +02:00 committed by GitHub
parent ce48f4c845
commit 37f8a63e39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 9 deletions

View file

@ -27,9 +27,9 @@ The :mod:`getpass` module provides two functions:
The *echo_char* argument controls how user input is displayed while typing. The *echo_char* argument controls how user input is displayed while typing.
If *echo_char* is ``None`` (default), input remains hidden. Otherwise, If *echo_char* is ``None`` (default), input remains hidden. Otherwise,
*echo_char* must be a printable ASCII string and each typed character *echo_char* must be a single printable ASCII character and each
is replaced by it. For example, ``echo_char='*'`` will display typed character is replaced by it. For example, ``echo_char='*'`` will
asterisks instead of the actual input. display asterisks instead of the actual input.
If echo free input is unavailable getpass() falls back to printing If echo free input is unavailable getpass() falls back to printing
a warning message to *stream* and reading from ``sys.stdin`` and a warning message to *stream* and reading from ``sys.stdin`` and

View file

@ -33,8 +33,8 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
prompt: Written on stream to ask for the input. Default: 'Password: ' prompt: Written on stream to ask for the input. Default: 'Password: '
stream: A writable file object to display the prompt. Defaults to stream: A writable file object to display the prompt. Defaults to
the tty. If no tty is available defaults to sys.stderr. the tty. If no tty is available defaults to sys.stderr.
echo_char: A string used to mask input (e.g., '*'). If None, input is echo_char: A single ASCII character to mask input (e.g., '*').
hidden. If None, input is hidden.
Returns: Returns:
The seKr3t input. The seKr3t input.
Raises: Raises:
@ -144,10 +144,19 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None):
def _check_echo_char(echo_char): def _check_echo_char(echo_char):
# ASCII excluding control characters # Single-character ASCII excluding control characters
if echo_char and not (echo_char.isprintable() and echo_char.isascii()): if echo_char is None:
raise ValueError("'echo_char' must be a printable ASCII string, " return
f"got: {echo_char!r}") if not isinstance(echo_char, str):
raise TypeError("'echo_char' must be a str or None, not "
f"{type(echo_char).__name__}")
if not (
len(echo_char) == 1
and echo_char.isprintable()
and echo_char.isascii()
):
raise ValueError("'echo_char' must be a single printable ASCII "
f"character, got: {echo_char!r}")
def _raw_input(prompt="", stream=None, input=None, echo_char=None): def _raw_input(prompt="", stream=None, input=None, echo_char=None):

View file

@ -201,5 +201,41 @@ class UnixGetpassTest(unittest.TestCase):
self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue()) self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue())
class GetpassEchoCharTest(unittest.TestCase):
def test_accept_none(self):
getpass._check_echo_char(None)
@support.subTests('echo_char', ["*", "A", " "])
def test_accept_single_printable_ascii(self, echo_char):
getpass._check_echo_char(echo_char)
def test_reject_empty_string(self):
self.assertRaises(ValueError, getpass.getpass, echo_char="")
@support.subTests('echo_char', ["***", "AA", "aA*!"])
def test_reject_multi_character_strings(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
@support.subTests('echo_char', [
'\N{LATIN CAPITAL LETTER AE}', # non-ASCII single character
'\N{HEAVY BLACK HEART}', # non-ASCII multibyte character
])
def test_reject_non_ascii(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
@support.subTests('echo_char', [
ch for ch in map(chr, range(0, 128))
if not ch.isprintable()
])
def test_reject_non_printable_characters(self, echo_char):
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
# TypeError Rejection
@support.subTests('echo_char', [b"*", 0, 0.0, [], {}])
def test_reject_non_string(self, echo_char):
self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -903,6 +903,7 @@ Jim Jewett
Pedro Diaz Jimenez Pedro Diaz Jimenez
Orjan Johansen Orjan Johansen
Fredrik Johansson Fredrik Johansson
Benjamin K. Johnson
Gregory K. Johnson Gregory K. Johnson
Kent Johnson Kent Johnson
Michael Johnson Michael Johnson

View file

@ -0,0 +1,2 @@
Raise :exc:`ValueError` when a multi-character string is passed to the
*echo_char* parameter of :func:`getpass.getpass`. Patch by Benjamin Johnson.