Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/security.py: 23%

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

65 statements  

1from __future__ import annotations 

2 

3import hashlib 

4import hmac 

5import os 

6import posixpath 

7import secrets 

8 

9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 

10DEFAULT_PBKDF2_ITERATIONS = 1_000_000 

11 

12_os_alt_seps: list[str] = list( 

13 sep for sep in [os.sep, os.altsep] if sep is not None and sep != "/" 

14) 

15# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html 

16_windows_device_files = { 

17 "AUX", 

18 "CON", 

19 "CONIN$", 

20 "CONOUT$", 

21 *(f"COM{c}" for c in "123456789¹²³"), 

22 *(f"LPT{c}" for c in "123456789¹²³"), 

23 "NUL", 

24 "PRN", 

25} 

26 

27 

28def gen_salt(length: int) -> str: 

29 """Generate a random string of SALT_CHARS with specified ``length``.""" 

30 if length <= 0: 

31 raise ValueError("Salt length must be at least 1.") 

32 

33 return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) 

34 

35 

36def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]: 

37 method, *args = method.split(":") 

38 salt_bytes = salt.encode() 

39 password_bytes = password.encode() 

40 

41 # Security note: If the method contains args, they came from a stored, 

42 # trusted hash previously generated by generate_password_hash. An attacker 

43 # who could modify them can already compromise the entire application. 

44 

45 if method == "scrypt": 

46 if not args: 

47 n = 2**15 

48 r = 8 

49 p = 1 

50 else: 

51 try: 

52 n, r, p = map(int, args) 

53 except ValueError: 

54 raise ValueError("'scrypt' takes 3 arguments.") from None 

55 

56 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed 

57 return ( 

58 hashlib.scrypt( 

59 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem 

60 ).hex(), 

61 f"scrypt:{n}:{r}:{p}", 

62 ) 

63 elif method == "pbkdf2": 

64 len_args = len(args) 

65 

66 if len_args == 0: 

67 hash_name = "sha256" 

68 iterations = DEFAULT_PBKDF2_ITERATIONS 

69 elif len_args == 1: 

70 hash_name = args[0] 

71 iterations = DEFAULT_PBKDF2_ITERATIONS 

72 elif len_args == 2: 

73 hash_name = args[0] 

74 iterations = int(args[1]) 

75 else: 

76 raise ValueError("'pbkdf2' takes 2 arguments.") 

77 

78 return ( 

79 hashlib.pbkdf2_hmac( 

80 hash_name, password_bytes, salt_bytes, iterations 

81 ).hex(), 

82 f"pbkdf2:{hash_name}:{iterations}", 

83 ) 

84 else: 

85 raise ValueError(f"Invalid hash method '{method}'.") 

86 

87 

88def generate_password_hash( 

89 password: str, method: str = "scrypt", salt_length: int = 16 

90) -> str: 

91 """Securely hash a password for storage. A password can be compared to a stored hash 

92 using :func:`check_password_hash`. 

93 

94 The following methods are supported: 

95 

96 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default 

97 is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`. 

98 - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``, 

99 the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`. 

100 

101 Default parameters may be updated to reflect current guidelines, and methods may be 

102 deprecated and removed if they are no longer considered secure. To migrate old 

103 hashes, you may generate a new hash when checking an old hash, or you may contact 

104 users with a link to reset their password. 

105 

106 :param password: The plaintext password. 

107 :param method: The key derivation function and parameters. 

108 :param salt_length: The number of characters to generate for the salt. 

109 

110 .. versionchanged:: 3.1 

111 The default iterations for pbkdf2 was increased to 1,000,000. 

112 

113 .. versionchanged:: 2.3 

114 Scrypt support was added. 

115 

116 .. versionchanged:: 2.3 

117 The default iterations for pbkdf2 was increased to 600,000. 

118 

119 .. versionchanged:: 2.3 

120 All plain hashes are deprecated and will not be supported in Werkzeug 3.0. 

121 """ 

122 salt = gen_salt(salt_length) 

123 h, actual_method = _hash_internal(method, salt, password) 

124 return f"{actual_method}${salt}${h}" 

125 

126 

127def check_password_hash(pwhash: str, password: str) -> bool: 

128 """Securely check that the given stored password hash, previously generated using 

129 :func:`generate_password_hash`, matches the given password. 

130 

131 Methods may be deprecated and removed if they are no longer considered secure. To 

132 migrate old hashes, you may generate a new hash when checking an old hash, or you 

133 may contact users with a link to reset their password. 

134 

135 :param pwhash: The hashed password. 

136 :param password: The plaintext password. 

137 

138 .. versionchanged:: 2.3 

139 All plain hashes are deprecated and will not be supported in Werkzeug 3.0. 

140 """ 

141 try: 

142 method, salt, hashval = pwhash.split("$", 2) 

143 except ValueError: 

144 return False 

145 

146 return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval) 

147 

148 

149def safe_join(directory: str, *untrusted: str) -> str | None: 

150 """Safely join zero or more untrusted path components to a trusted base 

151 directory to avoid escaping the base directory. Return ``None`` if the path 

152 is not safe. 

153 

154 The untrusted path is assumed to be from/for a URL, such as for serving 

155 files. Therefore, it should only use the forward slash ``/`` path separator, 

156 and will be joined using that separator. On Windows, the backslash ``\\`` 

157 separator is not allowed. 

158 

159 This only considers the path name, it does not validate if it does or does 

160 not exist, if it is a symlink, or other properties of the file or 

161 filesystem. It's up to the application to ensure that the contents of 

162 ``directory`` can be trusted. 

163 

164 On Windows, special device names such as ``CON`` and ``NUL`` are not 

165 allowed, as this is used for serving files with :func:`.send_from_directory` 

166 and those files will hang when read. 

167 

168 NTFS alternate data streams (ADS) like ``filename:stream`` are 

169 allowed. :func:`.secure_filename` will remove the ``:`` when saving. 

170 

171 :param directory: The trusted base directory. 

172 :param untrusted: The untrusted path components relative to the 

173 base directory. 

174 :return: A safe path, otherwise ``None``. 

175 

176 .. versionchanged:: 3.1.9 

177 Special device names with empty ADS stream markers are not allowed on 

178 Windows. 

179 

180 .. versionchanged:: 3.1.6 

181 Special device names in multi-segment paths are not allowed on Windows. 

182 

183 .. versionchanged:: 3.1.5 

184 More special device names, regardless of extension or trailing spaces, 

185 are not allowed on Windows. 

186 

187 .. versionchanged:: 3.1.4 

188 Special device names are not allowed on Windows. 

189 """ 

190 if not directory: 

191 # Ensure we end up with ./path if directory="" is given, 

192 # otherwise the first untrusted part could become trusted. 

193 directory = "." 

194 

195 parts = [directory] 

196 

197 for part in untrusted: 

198 if not part: 

199 continue 

200 

201 part = posixpath.normpath(part) 

202 

203 if ( 

204 os.path.isabs(part) 

205 # ntpath.isabs doesn't catch this 

206 or part.startswith("/") 

207 or part == ".." 

208 or part.startswith("../") 

209 or any(sep in part for sep in _os_alt_seps) 

210 or ( 

211 os.name == "nt" 

212 and any( 

213 p.partition(":")[0].partition(".")[0].strip().upper() 

214 in _windows_device_files 

215 for p in part.split("/") 

216 ) 

217 ) 

218 ): 

219 return None 

220 

221 parts.append(part) 

222 

223 return posixpath.join(*parts)