Skip to content

Commit 37f8a63

Browse files
miss-islingtonbenjaminjohnson01picnixzbrianschubert
authored
[3.14] gh-138514: getpass: restrict echo_char to a single ASCII character (GH-138591) (#138988)
Co-authored-by: Benjamin Johnson <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Brian Schubert <[email protected]>
1 parent ce48f4c commit 37f8a63

File tree

5 files changed

+57
-9
lines changed

5 files changed

+57
-9
lines changed

Doc/library/getpass.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ The :mod:`getpass` module provides two functions:
2727

2828
The *echo_char* argument controls how user input is displayed while typing.
2929
If *echo_char* is ``None`` (default), input remains hidden. Otherwise,
30-
*echo_char* must be a printable ASCII string and each typed character
31-
is replaced by it. For example, ``echo_char='*'`` will display
32-
asterisks instead of the actual input.
30+
*echo_char* must be a single printable ASCII character and each
31+
typed character is replaced by it. For example, ``echo_char='*'`` will
32+
display asterisks instead of the actual input.
3333

3434
If echo free input is unavailable getpass() falls back to printing
3535
a warning message to *stream* and reading from ``sys.stdin`` and

Lib/getpass.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ def unix_getpass(prompt='Password: ', stream=None, *, echo_char=None):
3333
prompt: Written on stream to ask for the input. Default: 'Password: '
3434
stream: A writable file object to display the prompt. Defaults to
3535
the tty. If no tty is available defaults to sys.stderr.
36-
echo_char: A string used to mask input (e.g., '*'). If None, input is
37-
hidden.
36+
echo_char: A single ASCII character to mask input (e.g., '*').
37+
If None, input is hidden.
3838
Returns:
3939
The seKr3t input.
4040
Raises:
@@ -144,10 +144,19 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None):
144144

145145

146146
def _check_echo_char(echo_char):
147-
# ASCII excluding control characters
148-
if echo_char and not (echo_char.isprintable() and echo_char.isascii()):
149-
raise ValueError("'echo_char' must be a printable ASCII string, "
150-
f"got: {echo_char!r}")
147+
# Single-character ASCII excluding control characters
148+
if echo_char is None:
149+
return
150+
if not isinstance(echo_char, str):
151+
raise TypeError("'echo_char' must be a str or None, not "
152+
f"{type(echo_char).__name__}")
153+
if not (
154+
len(echo_char) == 1
155+
and echo_char.isprintable()
156+
and echo_char.isascii()
157+
):
158+
raise ValueError("'echo_char' must be a single printable ASCII "
159+
f"character, got: {echo_char!r}")
151160

152161

153162
def _raw_input(prompt="", stream=None, input=None, echo_char=None):

Lib/test/test_getpass.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,5 +201,41 @@ def test_control_chars_with_echo_char(self):
201201
self.assertEqual('Password: *******\x08 \x08', mock_output.getvalue())
202202

203203

204+
class GetpassEchoCharTest(unittest.TestCase):
205+
206+
def test_accept_none(self):
207+
getpass._check_echo_char(None)
208+
209+
@support.subTests('echo_char', ["*", "A", " "])
210+
def test_accept_single_printable_ascii(self, echo_char):
211+
getpass._check_echo_char(echo_char)
212+
213+
def test_reject_empty_string(self):
214+
self.assertRaises(ValueError, getpass.getpass, echo_char="")
215+
216+
@support.subTests('echo_char', ["***", "AA", "aA*!"])
217+
def test_reject_multi_character_strings(self, echo_char):
218+
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
219+
220+
@support.subTests('echo_char', [
221+
'\N{LATIN CAPITAL LETTER AE}', # non-ASCII single character
222+
'\N{HEAVY BLACK HEART}', # non-ASCII multibyte character
223+
])
224+
def test_reject_non_ascii(self, echo_char):
225+
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
226+
227+
@support.subTests('echo_char', [
228+
ch for ch in map(chr, range(0, 128))
229+
if not ch.isprintable()
230+
])
231+
def test_reject_non_printable_characters(self, echo_char):
232+
self.assertRaises(ValueError, getpass.getpass, echo_char=echo_char)
233+
234+
# TypeError Rejection
235+
@support.subTests('echo_char', [b"*", 0, 0.0, [], {}])
236+
def test_reject_non_string(self, echo_char):
237+
self.assertRaises(TypeError, getpass.getpass, echo_char=echo_char)
238+
239+
204240
if __name__ == "__main__":
205241
unittest.main()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,7 @@ Jim Jewett
903903
Pedro Diaz Jimenez
904904
Orjan Johansen
905905
Fredrik Johansson
906+
Benjamin K. Johnson
906907
Gregory K. Johnson
907908
Kent Johnson
908909
Michael Johnson
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Raise :exc:`ValueError` when a multi-character string is passed to the
2+
*echo_char* parameter of :func:`getpass.getpass`. Patch by Benjamin Johnson.

0 commit comments

Comments
 (0)