Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/security.py: 21%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import hashlib
4import hmac
5import os
6import posixpath
7import secrets
9DEFAULT_PBKDF2_ITERATIONS = 1_000_000
11_os_alt_seps: list[str] = list(
12 sep for sep in [os.sep, os.altsep] if sep is not None and sep != "/"
13)
14# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html
15_windows_device_files = {
16 "AUX",
17 "CON",
18 "CONIN$",
19 "CONOUT$",
20 *(f"COM{c}" for c in "123456789¹²³"),
21 *(f"LPT{c}" for c in "123456789¹²³"),
22 "NUL",
23 "PRN",
24}
27def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
28 method, *args = method.split(":")
29 salt_bytes = salt.encode()
30 password_bytes = password.encode()
32 # Security note: If the method contains args, they came from a stored,
33 # trusted hash previously generated by generate_password_hash. An attacker
34 # who could modify them can already compromise the entire application.
36 if method == "scrypt":
37 if not args:
38 n = 2**15
39 r = 8
40 p = 1
41 else:
42 try:
43 n, r, p = map(int, args)
44 except ValueError:
45 raise ValueError("'scrypt' takes 3 arguments.") from None
47 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
48 return (
49 hashlib.scrypt(
50 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
51 ).hex(),
52 f"scrypt:{n}:{r}:{p}",
53 )
54 elif method == "pbkdf2":
55 len_args = len(args)
57 if len_args == 0:
58 hash_name = "sha256"
59 iterations = DEFAULT_PBKDF2_ITERATIONS
60 elif len_args == 1:
61 hash_name = args[0]
62 iterations = DEFAULT_PBKDF2_ITERATIONS
63 elif len_args == 2:
64 hash_name = args[0]
65 iterations = int(args[1])
66 else:
67 raise ValueError("'pbkdf2' takes 2 arguments.")
69 return (
70 hashlib.pbkdf2_hmac(
71 hash_name, password_bytes, salt_bytes, iterations
72 ).hex(),
73 f"pbkdf2:{hash_name}:{iterations}",
74 )
75 else:
76 raise ValueError(f"Invalid hash method '{method}'.")
79def generate_password_hash(
80 password: str, method: str = "scrypt", salt_length: int = 16
81) -> str:
82 """Securely hash a password for storage. A password can be compared to a stored hash
83 using :func:`check_password_hash`.
85 The following methods are supported:
87 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
88 is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
89 - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
90 the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
92 Default parameters may be updated to reflect current guidelines, and methods may be
93 deprecated and removed if they are no longer considered secure. To migrate old
94 hashes, you may generate a new hash when checking an old hash, or you may contact
95 users with a link to reset their password.
97 :param password: The plaintext password.
98 :param method: The key derivation function and parameters.
99 :param salt_length: The number of characters to generate for the salt.
101 .. versionchanged:: 3.1
102 The default iterations for pbkdf2 was increased to 1,000,000.
104 .. versionchanged:: 3.0
105 All plain hashes are no longer supported.
107 .. versionchanged:: 2.3
108 Scrypt support was added.
110 .. versionchanged:: 2.3
111 The default iterations for pbkdf2 was increased to 600,000.
112 """
113 if salt_length <= 0:
114 raise ValueError("Salt length must be at least 1.")
116 salt = secrets.token_urlsafe(salt_length)
117 h, actual_method = _hash_internal(method, salt, password)
118 return f"{actual_method}${salt}${h}"
121def check_password_hash(pwhash: str, password: str) -> bool:
122 """Securely check that the given stored password hash, previously generated using
123 :func:`generate_password_hash`, matches the given password.
125 Methods may be deprecated and removed if they are no longer considered secure. To
126 migrate old hashes, you may generate a new hash when checking an old hash, or you
127 may contact users with a link to reset their password.
129 :param pwhash: The hashed password.
130 :param password: The plaintext password.
132 .. versionchanged:: 3.0
133 All plain hashes are no longer supported.
134 """
135 try:
136 method, salt, hashval = pwhash.split("$", 2)
137 except ValueError:
138 return False
140 return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
143def safe_join(directory: str, *untrusted: str) -> str | None:
144 """Safely join zero or more untrusted path components to a trusted base
145 directory to avoid escaping the base directory. Return ``None`` if the path
146 is not safe.
148 The untrusted path is assumed to be from/for a URL, such as for serving
149 files. Therefore, it should only use the forward slash ``/`` path separator,
150 and will be joined using that separator. On Windows, the backslash ``\\``
151 separator is not allowed.
153 This only considers the path name, it does not validate if it does or does
154 not exist, if it is a symlink, or other properties of the file or
155 filesystem. It's up to the application to ensure that the contents of
156 ``directory`` can be trusted.
158 On Windows, special device names such as ``CON`` and ``NUL`` are not
159 allowed, as this is used for serving files with :func:`.send_from_directory`
160 and those files will hang when read.
162 NTFS alternate data streams (ADS) like ``filename:stream`` are
163 allowed. :func:`.secure_filename` will remove the ``:`` when saving.
165 :param directory: The trusted base directory.
166 :param untrusted: The untrusted path components relative to the
167 base directory.
168 :return: A safe path, otherwise ``None``.
170 .. versionchanged:: 3.1.9
171 Special device names with empty ADS stream markers are not allowed on
172 Windows.
174 .. versionchanged:: 3.1.6
175 Special device names in multi-segment paths are not allowed on Windows.
177 .. versionchanged:: 3.1.5
178 More special device names, regardless of extension or trailing spaces,
179 are not allowed on Windows.
181 .. versionchanged:: 3.1.4
182 Special device names are not allowed on Windows.
183 """
184 if not directory:
185 # Ensure we end up with ./path if directory="" is given,
186 # otherwise the first untrusted part could become trusted.
187 directory = "."
189 parts = [directory]
191 for part in untrusted:
192 if not part:
193 continue
195 part = posixpath.normpath(part)
197 if (
198 os.path.isabs(part)
199 # ntpath.isabs doesn't catch this
200 or part.startswith("/")
201 or part == ".."
202 or part.startswith("../")
203 or any(sep in part for sep in _os_alt_seps)
204 or (
205 os.name == "nt"
206 and any(
207 p.partition(":")[0].partition(".")[0].strip().upper()
208 in _windows_device_files
209 for p in part.split("/")
210 )
211 )
212 ):
213 return None
215 parts.append(part)
217 return posixpath.join(*parts)