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 if method == "scrypt": 

42 if not args: 

43 n = 2**15 

44 r = 8 

45 p = 1 

46 else: 

47 try: 

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

49 except ValueError: 

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

51 

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

53 return ( 

54 hashlib.scrypt( 

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

56 ).hex(), 

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

58 ) 

59 elif method == "pbkdf2": 

60 len_args = len(args) 

61 

62 if len_args == 0: 

63 hash_name = "sha256" 

64 iterations = DEFAULT_PBKDF2_ITERATIONS 

65 elif len_args == 1: 

66 hash_name = args[0] 

67 iterations = DEFAULT_PBKDF2_ITERATIONS 

68 elif len_args == 2: 

69 hash_name = args[0] 

70 iterations = int(args[1]) 

71 else: 

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

73 

74 return ( 

75 hashlib.pbkdf2_hmac( 

76 hash_name, password_bytes, salt_bytes, iterations 

77 ).hex(), 

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

79 ) 

80 else: 

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

82 

83 

84def generate_password_hash( 

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

86) -> str: 

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

88 using :func:`check_password_hash`. 

89 

90 The following methods are supported: 

91 

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

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

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

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

96 

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

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

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

100 users with a link to reset their password. 

101 

102 :param password: The plaintext password. 

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

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

105 

106 .. versionchanged:: 3.1 

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

108 

109 .. versionchanged:: 2.3 

110 Scrypt support was added. 

111 

112 .. versionchanged:: 2.3 

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

114 

115 .. versionchanged:: 2.3 

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

117 """ 

118 salt = gen_salt(salt_length) 

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

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

121 

122 

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

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

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

126 

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

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

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

130 

131 :param pwhash: The hashed password. 

132 :param password: The plaintext password. 

133 

134 .. versionchanged:: 2.3 

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

136 """ 

137 try: 

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

139 except ValueError: 

140 return False 

141 

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

143 

144 

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

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

147 directory to avoid escaping the base directory. 

148 

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

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

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

152 separator is not allowed. 

153 

154 :param directory: The trusted base directory. 

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

156 base directory. 

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

158 

159 .. versionchanged:: 3.1.6 

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

161 

162 .. versionchanged:: 3.1.5 

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

164 are not allowed on Windows. 

165 

166 .. versionchanged:: 3.1.4 

167 Special device names are not allowed on Windows. 

168 """ 

169 if not directory: 

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

171 # otherwise the first untrusted part could become trusted. 

172 directory = "." 

173 

174 parts = [directory] 

175 

176 for part in untrusted: 

177 if not part: 

178 continue 

179 

180 part = posixpath.normpath(part) 

181 

182 if ( 

183 os.path.isabs(part) 

184 # ntpath.isabs doesn't catch this 

185 or part.startswith("/") 

186 or part == ".." 

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

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

189 or ( 

190 os.name == "nt" 

191 and any( 

192 p.partition(".")[0].strip().upper() in _windows_device_files 

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

194 ) 

195 ) 

196 ): 

197 return None 

198 

199 parts.append(part) 

200 

201 return posixpath.join(*parts)