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

62 statements  

1from __future__ import annotations 

2 

3import hashlib 

4import hmac 

5import os 

6import posixpath 

7import secrets 

8 

9DEFAULT_PBKDF2_ITERATIONS = 1_000_000 

10 

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} 

25 

26 

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() 

31 

32 if method == "scrypt": 

33 if not args: 

34 n = 2**15 

35 r = 8 

36 p = 1 

37 else: 

38 try: 

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

40 except ValueError: 

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

42 

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

44 return ( 

45 hashlib.scrypt( 

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

47 ).hex(), 

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

49 ) 

50 elif method == "pbkdf2": 

51 len_args = len(args) 

52 

53 if len_args == 0: 

54 hash_name = "sha256" 

55 iterations = DEFAULT_PBKDF2_ITERATIONS 

56 elif len_args == 1: 

57 hash_name = args[0] 

58 iterations = DEFAULT_PBKDF2_ITERATIONS 

59 elif len_args == 2: 

60 hash_name = args[0] 

61 iterations = int(args[1]) 

62 else: 

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

64 

65 return ( 

66 hashlib.pbkdf2_hmac( 

67 hash_name, password_bytes, salt_bytes, iterations 

68 ).hex(), 

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

70 ) 

71 else: 

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

73 

74 

75def generate_password_hash( 

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

77) -> str: 

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

79 using :func:`check_password_hash`. 

80 

81 The following methods are supported: 

82 

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

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

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

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

87 

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

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

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

91 users with a link to reset their password. 

92 

93 :param password: The plaintext password. 

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

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

96 

97 .. versionchanged:: 3.1 

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

99 

100 .. versionchanged:: 3.0 

101 All plain hashes are no longer supported. 

102 

103 .. versionchanged:: 2.3 

104 Scrypt support was added. 

105 

106 .. versionchanged:: 2.3 

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

108 """ 

109 if salt_length <= 0: 

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

111 

112 salt = secrets.token_urlsafe(salt_length) 

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

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

115 

116 

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

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

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

120 

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

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

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

124 

125 :param pwhash: The hashed password. 

126 :param password: The plaintext password. 

127 

128 .. versionchanged:: 3.0 

129 All plain hashes are no longer supported. 

130 """ 

131 try: 

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

133 except ValueError: 

134 return False 

135 

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

137 

138 

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

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

141 directory to avoid escaping the base directory. 

142 

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

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

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

146 separator is not allowed. 

147 

148 :param directory: The trusted base directory. 

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

150 base directory. 

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

152 

153 .. versionchanged:: 3.1.6 

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

155 

156 .. versionchanged:: 3.1.5 

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

158 are not allowed on Windows. 

159 

160 .. versionchanged:: 3.1.4 

161 Special device names are not allowed on Windows. 

162 """ 

163 if not directory: 

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

165 # otherwise the first untrusted part could become trusted. 

166 directory = "." 

167 

168 parts = [directory] 

169 

170 for part in untrusted: 

171 if not part: 

172 continue 

173 

174 part = posixpath.normpath(part) 

175 

176 if ( 

177 os.path.isabs(part) 

178 # ntpath.isabs doesn't catch this 

179 or part.startswith("/") 

180 or part == ".." 

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

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

183 or ( 

184 os.name == "nt" 

185 and any( 

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

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

188 ) 

189 ) 

190 ): 

191 return None 

192 

193 parts.append(part) 

194 

195 return posixpath.join(*parts)