]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-138514: getpass: restrict `echo_char` to a single ASCII character (GH-13859...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Wed, 17 Sep 2025 14:20:45 +0000 (16:20 +0200)
committerGitHub <noreply@github.com>
Wed, 17 Sep 2025 14:20:45 +0000 (17:20 +0300)
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>
Doc/library/getpass.rst
Lib/getpass.py
Lib/test/test_getpass.py
Misc/ACKS
Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst [new file with mode: 0644]

index af9c9e9f39d9a686764a15ca34a20ef2327a3a8b..a0c0c6dee2d513168a4bbd565f9cb4c2341832cc 100644 (file)
@@ -27,9 +27,9 @@ The :mod:`getpass` module provides two functions:
 
    The *echo_char* argument controls how user input is displayed while typing.
    If *echo_char* is ``None`` (default), input remains hidden. Otherwise,
-   *echo_char* must be a printable ASCII string and each typed character
-   is replaced by it. For example, ``echo_char='*'`` will display
-   asterisks instead of the actual input.
+   *echo_char* must be a single printable ASCII character and each
+   typed character is replaced by it. For example, ``echo_char='*'`` will
+   display asterisks instead of the actual input.
 
    If echo free input is unavailable getpass() falls back to printing
    a warning message to *stream* and reading from ``sys.stdin`` and
index 1dd40e25e0906888dc1e72790b5b62d910eb0ad6..3d9bb1f0d146a48ba9622f84b008e7f69f7584b9 100644 (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: '
       stream: A writable file object to display the prompt.  Defaults to
               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
-                hidden.
+      echo_char: A single ASCII character to mask input (e.g., '*').
+              If None, input is hidden.
     Returns:
       The seKr3t input.
     Raises:
@@ -144,10 +144,19 @@ def fallback_getpass(prompt='Password: ', stream=None, *, echo_char=None):
 
 
 def _check_echo_char(echo_char):
-    # ASCII excluding control characters
-    if echo_char and not (echo_char.isprintable() and echo_char.isascii()):
-        raise ValueError("'echo_char' must be a printable ASCII string, "
-                         f"got: {echo_char!r}")
+    # Single-character ASCII excluding control characters
+    if echo_char is None:
+        return
+    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):
index ab36535a1cfa8a98d6b283eb4bc057799d8f6a20..9c3def2c3be59b1c9d29c5684fbe67b0b8674072 100644 (file)
@@ -201,5 +201,41 @@ class UnixGetpassTest(unittest.TestCase):
         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__":
     unittest.main()
index 83bc62726eecc97441f5d848cf5737dc1af6de31..ee3d66a04f0217376194f62d5e2a99f65bd968f6 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -903,6 +903,7 @@ Jim Jewett
 Pedro Diaz Jimenez
 Orjan Johansen
 Fredrik Johansson
+Benjamin K. Johnson
 Gregory K. Johnson
 Kent Johnson
 Michael Johnson
diff --git a/Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst b/Misc/NEWS.d/next/Library/2025-09-06-11-26-21.gh-issue-138514.66ltOb.rst
new file mode 100644 (file)
index 0000000..75151ea
--- /dev/null
@@ -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.