security.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. from __future__ import annotations
  2. import hashlib
  3. import hmac
  4. import os
  5. import posixpath
  6. import secrets
  7. SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  8. DEFAULT_PBKDF2_ITERATIONS = 1_000_000
  9. _os_alt_seps: list[str] = list(
  10. sep for sep in [os.sep, os.altsep] if sep is not None and sep != "/"
  11. )
  12. # https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html
  13. _windows_device_files = {
  14. "AUX",
  15. "CON",
  16. "CONIN$",
  17. "CONOUT$",
  18. *(f"COM{c}" for c in "123456789¹²³"),
  19. *(f"LPT{c}" for c in "123456789¹²³"),
  20. "NUL",
  21. "PRN",
  22. }
  23. def gen_salt(length: int) -> str:
  24. """Generate a random string of SALT_CHARS with specified ``length``."""
  25. if length <= 0:
  26. raise ValueError("Salt length must be at least 1.")
  27. return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
  28. def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
  29. method, *args = method.split(":")
  30. salt_bytes = salt.encode()
  31. password_bytes = password.encode()
  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. maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
  43. return (
  44. hashlib.scrypt(
  45. password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
  46. ).hex(),
  47. f"scrypt:{n}:{r}:{p}",
  48. )
  49. elif method == "pbkdf2":
  50. len_args = len(args)
  51. if len_args == 0:
  52. hash_name = "sha256"
  53. iterations = DEFAULT_PBKDF2_ITERATIONS
  54. elif len_args == 1:
  55. hash_name = args[0]
  56. iterations = DEFAULT_PBKDF2_ITERATIONS
  57. elif len_args == 2:
  58. hash_name = args[0]
  59. iterations = int(args[1])
  60. else:
  61. raise ValueError("'pbkdf2' takes 2 arguments.")
  62. return (
  63. hashlib.pbkdf2_hmac(
  64. hash_name, password_bytes, salt_bytes, iterations
  65. ).hex(),
  66. f"pbkdf2:{hash_name}:{iterations}",
  67. )
  68. else:
  69. raise ValueError(f"Invalid hash method '{method}'.")
  70. def generate_password_hash(
  71. password: str, method: str = "scrypt", salt_length: int = 16
  72. ) -> str:
  73. """Securely hash a password for storage. A password can be compared to a stored hash
  74. using :func:`check_password_hash`.
  75. The following methods are supported:
  76. - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
  77. is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
  78. - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
  79. the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
  80. Default parameters may be updated to reflect current guidelines, and methods may be
  81. deprecated and removed if they are no longer considered secure. To migrate old
  82. hashes, you may generate a new hash when checking an old hash, or you may contact
  83. users with a link to reset their password.
  84. :param password: The plaintext password.
  85. :param method: The key derivation function and parameters.
  86. :param salt_length: The number of characters to generate for the salt.
  87. .. versionchanged:: 3.1
  88. The default iterations for pbkdf2 was increased to 1,000,000.
  89. .. versionchanged:: 2.3
  90. Scrypt support was added.
  91. .. versionchanged:: 2.3
  92. The default iterations for pbkdf2 was increased to 600,000.
  93. .. versionchanged:: 2.3
  94. All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
  95. """
  96. salt = gen_salt(salt_length)
  97. h, actual_method = _hash_internal(method, salt, password)
  98. return f"{actual_method}${salt}${h}"
  99. def check_password_hash(pwhash: str, password: str) -> bool:
  100. """Securely check that the given stored password hash, previously generated using
  101. :func:`generate_password_hash`, matches the given password.
  102. Methods may be deprecated and removed if they are no longer considered secure. To
  103. migrate old hashes, you may generate a new hash when checking an old hash, or you
  104. may contact users with a link to reset their password.
  105. :param pwhash: The hashed password.
  106. :param password: The plaintext password.
  107. .. versionchanged:: 2.3
  108. All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
  109. """
  110. try:
  111. method, salt, hashval = pwhash.split("$", 2)
  112. except ValueError:
  113. return False
  114. return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
  115. def safe_join(directory: str, *untrusted: str) -> str | None:
  116. """Safely join zero or more untrusted path components to a trusted base
  117. directory to avoid escaping the base directory.
  118. The untrusted path is assumed to be from/for a URL, such as for serving
  119. files. Therefore, it should only use the forward slash ``/`` path separator,
  120. and will be joined using that separator. On Windows, the backslash ``\\``
  121. separator is not allowed.
  122. :param directory: The trusted base directory.
  123. :param untrusted: The untrusted path components relative to the
  124. base directory.
  125. :return: A safe path, otherwise ``None``.
  126. .. versionchanged:: 3.1.6
  127. Special device names in multi-segment paths are not allowed on Windows.
  128. .. versionchanged:: 3.1.5
  129. More special device names, regardless of extension or trailing spaces,
  130. are not allowed on Windows.
  131. .. versionchanged:: 3.1.4
  132. Special device names are not allowed on Windows.
  133. """
  134. if not directory:
  135. # Ensure we end up with ./path if directory="" is given,
  136. # otherwise the first untrusted part could become trusted.
  137. directory = "."
  138. parts = [directory]
  139. for part in untrusted:
  140. if not part:
  141. continue
  142. part = posixpath.normpath(part)
  143. if (
  144. os.path.isabs(part)
  145. # ntpath.isabs doesn't catch this
  146. or part.startswith("/")
  147. or part == ".."
  148. or part.startswith("../")
  149. or any(sep in part for sep in _os_alt_seps)
  150. or (
  151. os.name == "nt"
  152. and any(
  153. p.partition(".")[0].strip().upper() in _windows_device_files
  154. for p in part.split("/")
  155. )
  156. )
  157. ):
  158. return None
  159. parts.append(part)
  160. return posixpath.join(*parts)