__init__.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. from __future__ import annotations
  2. import getpass
  3. import hashlib
  4. import json
  5. import os
  6. import pkgutil
  7. import re
  8. import sys
  9. import time
  10. import typing as t
  11. import uuid
  12. from contextlib import ExitStack
  13. from io import BytesIO
  14. from itertools import chain
  15. from multiprocessing import Value
  16. from os.path import basename
  17. from os.path import join
  18. from zlib import adler32
  19. from .._internal import _log
  20. from ..exceptions import NotFound
  21. from ..exceptions import SecurityError
  22. from ..http import parse_cookie
  23. from ..sansio.utils import host_is_trusted
  24. from ..security import gen_salt
  25. from ..utils import send_file
  26. from ..wrappers.request import Request
  27. from ..wrappers.response import Response
  28. from .console import Console
  29. from .tbtools import DebugFrameSummary
  30. from .tbtools import DebugTraceback
  31. from .tbtools import render_console_html
  32. if t.TYPE_CHECKING:
  33. from _typeshed.wsgi import StartResponse
  34. from _typeshed.wsgi import WSGIApplication
  35. from _typeshed.wsgi import WSGIEnvironment
  36. # A week
  37. PIN_TIME = 60 * 60 * 24 * 7
  38. def hash_pin(pin: str) -> str:
  39. return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12]
  40. _machine_id: str | bytes | None = None
  41. def get_machine_id() -> str | bytes | None:
  42. global _machine_id
  43. if _machine_id is not None:
  44. return _machine_id
  45. def _generate() -> str | bytes | None:
  46. linux = b""
  47. # machine-id is stable across boots, boot_id is not.
  48. for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
  49. try:
  50. with open(filename, "rb") as f:
  51. value = f.readline().strip()
  52. except OSError:
  53. continue
  54. if value:
  55. linux += value
  56. break
  57. # Containers share the same machine id, add some cgroup
  58. # information. This is used outside containers too but should be
  59. # relatively stable across boots.
  60. try:
  61. with open("/proc/self/cgroup", "rb") as f:
  62. linux += f.readline().strip().rpartition(b"/")[2]
  63. except OSError:
  64. pass
  65. if linux:
  66. return linux
  67. # On OS X, use ioreg to get the computer's serial number.
  68. try:
  69. # subprocess may not be available, e.g. Google App Engine
  70. # https://github.com/pallets/werkzeug/issues/925
  71. from subprocess import PIPE
  72. from subprocess import Popen
  73. dump = Popen(
  74. ["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
  75. ).communicate()[0]
  76. match = re.search(b'"serial-number" = <([^>]+)', dump)
  77. if match is not None:
  78. return match.group(1)
  79. except (OSError, ImportError):
  80. pass
  81. # On Windows, use winreg to get the machine guid.
  82. if sys.platform == "win32":
  83. import winreg
  84. try:
  85. with winreg.OpenKey(
  86. winreg.HKEY_LOCAL_MACHINE,
  87. "SOFTWARE\\Microsoft\\Cryptography",
  88. 0,
  89. winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
  90. ) as rk:
  91. guid: str | bytes
  92. guid_type: int
  93. guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")
  94. if guid_type == winreg.REG_SZ:
  95. return guid.encode()
  96. return guid
  97. except OSError:
  98. pass
  99. return None
  100. _machine_id = _generate()
  101. return _machine_id
  102. class _ConsoleFrame:
  103. """Helper class so that we can reuse the frame console code for the
  104. standalone console.
  105. """
  106. def __init__(self, namespace: dict[str, t.Any]):
  107. self.console = Console(namespace)
  108. self.id = 0
  109. def eval(self, code: str) -> t.Any:
  110. return self.console.eval(code)
  111. def get_pin_and_cookie_name(
  112. app: WSGIApplication,
  113. ) -> tuple[str, str] | tuple[None, None]:
  114. """Given an application object this returns a semi-stable 9 digit pin
  115. code and a random key. The hope is that this is stable between
  116. restarts to not make debugging particularly frustrating. If the pin
  117. was forcefully disabled this returns `None`.
  118. Second item in the resulting tuple is the cookie name for remembering.
  119. """
  120. pin = os.environ.get("WERKZEUG_DEBUG_PIN")
  121. rv = None
  122. num = None
  123. # Pin was explicitly disabled
  124. if pin == "off":
  125. return None, None
  126. # Pin was provided explicitly
  127. if pin is not None and pin.replace("-", "").isdecimal():
  128. # If there are separators in the pin, return it directly
  129. if "-" in pin:
  130. rv = pin
  131. else:
  132. num = pin
  133. modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
  134. username: str | None
  135. try:
  136. # getuser imports the pwd module, which does not exist in Google
  137. # App Engine. It may also raise a KeyError if the UID does not
  138. # have a username, such as in Docker.
  139. username = getpass.getuser()
  140. # Python >= 3.13 only raises OSError
  141. except (ImportError, KeyError, OSError):
  142. username = None
  143. mod = sys.modules.get(modname)
  144. # This information only exists to make the cookie unique on the
  145. # computer, not as a security feature.
  146. probably_public_bits = [
  147. username,
  148. modname,
  149. getattr(app, "__name__", type(app).__name__),
  150. getattr(mod, "__file__", None),
  151. ]
  152. # This information is here to make it harder for an attacker to
  153. # guess the cookie name. They are unlikely to be contained anywhere
  154. # within the unauthenticated debug page.
  155. private_bits = [str(uuid.getnode()), get_machine_id()]
  156. h = hashlib.sha1()
  157. for bit in chain(probably_public_bits, private_bits):
  158. if not bit:
  159. continue
  160. if isinstance(bit, str):
  161. bit = bit.encode()
  162. h.update(bit)
  163. h.update(b"cookiesalt")
  164. cookie_name = f"__wzd{h.hexdigest()[:20]}"
  165. # If we need to generate a pin we salt it a bit more so that we don't
  166. # end up with the same value and generate out 9 digits
  167. if num is None:
  168. h.update(b"pinsalt")
  169. num = f"{int(h.hexdigest(), 16):09d}"[:9]
  170. # Format the pincode in groups of digits for easier remembering if
  171. # we don't have a result yet.
  172. if rv is None:
  173. for group_size in 5, 4, 3:
  174. if len(num) % group_size == 0:
  175. rv = "-".join(
  176. num[x : x + group_size].rjust(group_size, "0")
  177. for x in range(0, len(num), group_size)
  178. )
  179. break
  180. else:
  181. rv = num
  182. return rv, cookie_name
  183. class DebuggedApplication:
  184. """Enables debugging support for a given application::
  185. from werkzeug.debug import DebuggedApplication
  186. from myapp import app
  187. app = DebuggedApplication(app, evalex=True)
  188. The ``evalex`` argument allows evaluating expressions in any frame
  189. of a traceback. This works by preserving each frame with its local
  190. state. Some state, such as context globals, cannot be restored with
  191. the frame by default. When ``evalex`` is enabled,
  192. ``environ["werkzeug.debug.preserve_context"]`` will be a callable
  193. that takes a context manager, and can be called multiple times.
  194. Each context manager will be entered before evaluating code in the
  195. frame, then exited again, so they can perform setup and cleanup for
  196. each call.
  197. :param app: the WSGI application to run debugged.
  198. :param evalex: enable exception evaluation feature (interactive
  199. debugging). This requires a non-forking server.
  200. :param request_key: The key that points to the request object in this
  201. environment. This parameter is ignored in current
  202. versions.
  203. :param console_path: the URL for a general purpose console.
  204. :param console_init_func: the function that is executed before starting
  205. the general purpose console. The return value
  206. is used as initial namespace.
  207. :param show_hidden_frames: by default hidden traceback frames are skipped.
  208. You can show them by setting this parameter
  209. to `True`.
  210. :param pin_security: can be used to disable the pin based security system.
  211. :param pin_logging: enables the logging of the pin system.
  212. .. versionchanged:: 2.2
  213. Added the ``werkzeug.debug.preserve_context`` environ key.
  214. """
  215. _pin: str
  216. _pin_cookie: str
  217. def __init__(
  218. self,
  219. app: WSGIApplication,
  220. evalex: bool = False,
  221. request_key: str = "werkzeug.request",
  222. console_path: str = "/console",
  223. console_init_func: t.Callable[[], dict[str, t.Any]] | None = None,
  224. show_hidden_frames: bool = False,
  225. pin_security: bool = True,
  226. pin_logging: bool = True,
  227. ) -> None:
  228. if not console_init_func:
  229. console_init_func = None
  230. self.app = app
  231. self.evalex = evalex
  232. self.frames: dict[int, DebugFrameSummary | _ConsoleFrame] = {}
  233. self.frame_contexts: dict[int, list[t.ContextManager[None]]] = {}
  234. self.request_key = request_key
  235. self.console_path = console_path
  236. self.console_init_func = console_init_func
  237. self.show_hidden_frames = show_hidden_frames
  238. self.secret = gen_salt(20)
  239. self._failed_pin_auth = Value("B")
  240. self.pin_logging = pin_logging
  241. if pin_security:
  242. # Print out the pin for the debugger on standard out.
  243. if os.environ.get("WERKZEUG_RUN_MAIN") == "true" and pin_logging:
  244. _log("warning", " * Debugger is active!")
  245. if self.pin is None:
  246. _log("warning", " * Debugger PIN disabled. DEBUGGER UNSECURED!")
  247. else:
  248. _log("info", " * Debugger PIN: %s", self.pin)
  249. else:
  250. self.pin = None
  251. self.trusted_hosts: list[str] = [".localhost", "127.0.0.1"]
  252. """List of domains to allow requests to the debugger from. A leading dot
  253. allows all subdomains. This only allows ``".localhost"`` domains by
  254. default.
  255. .. versionadded:: 3.0.3
  256. """
  257. @property
  258. def pin(self) -> str | None:
  259. if not hasattr(self, "_pin"):
  260. pin_cookie = get_pin_and_cookie_name(self.app)
  261. self._pin, self._pin_cookie = pin_cookie # type: ignore
  262. return self._pin
  263. @pin.setter
  264. def pin(self, value: str) -> None:
  265. self._pin = value
  266. @property
  267. def pin_cookie_name(self) -> str:
  268. """The name of the pin cookie."""
  269. if not hasattr(self, "_pin_cookie"):
  270. pin_cookie = get_pin_and_cookie_name(self.app)
  271. self._pin, self._pin_cookie = pin_cookie # type: ignore
  272. return self._pin_cookie
  273. def debug_application(
  274. self, environ: WSGIEnvironment, start_response: StartResponse
  275. ) -> t.Iterator[bytes]:
  276. """Run the application and conserve the traceback frames."""
  277. contexts: list[t.ContextManager[t.Any]] = []
  278. if self.evalex:
  279. environ["werkzeug.debug.preserve_context"] = contexts.append
  280. app_iter = None
  281. try:
  282. app_iter = self.app(environ, start_response)
  283. yield from app_iter
  284. if hasattr(app_iter, "close"):
  285. app_iter.close()
  286. except Exception as e:
  287. if hasattr(app_iter, "close"):
  288. app_iter.close() # type: ignore
  289. tb = DebugTraceback(e, skip=1, hide=not self.show_hidden_frames)
  290. for frame in tb.all_frames:
  291. self.frames[id(frame)] = frame
  292. self.frame_contexts[id(frame)] = contexts
  293. is_trusted = bool(self.check_pin_trust(environ))
  294. html = tb.render_debugger_html(
  295. evalex=self.evalex and self.check_host_trust(environ),
  296. secret=self.secret,
  297. evalex_trusted=is_trusted,
  298. )
  299. response = Response(html, status=500, mimetype="text/html")
  300. try:
  301. yield from response(environ, start_response)
  302. except Exception:
  303. # if we end up here there has been output but an error
  304. # occurred. in that situation we can do nothing fancy any
  305. # more, better log something into the error log and fall
  306. # back gracefully.
  307. environ["wsgi.errors"].write(
  308. "Debugging middleware caught exception in streamed "
  309. "response at a point where response headers were already "
  310. "sent.\n"
  311. )
  312. environ["wsgi.errors"].write("".join(tb.render_traceback_text()))
  313. def execute_command(
  314. self,
  315. request: Request,
  316. command: str,
  317. frame: DebugFrameSummary | _ConsoleFrame,
  318. ) -> Response:
  319. """Execute a command in a console."""
  320. if not self.check_host_trust(request.environ):
  321. return SecurityError() # type: ignore[return-value]
  322. contexts = self.frame_contexts.get(id(frame), [])
  323. with ExitStack() as exit_stack:
  324. for cm in contexts:
  325. exit_stack.enter_context(cm)
  326. return Response(frame.eval(command), mimetype="text/html")
  327. def display_console(self, request: Request) -> Response:
  328. """Display a standalone shell."""
  329. if not self.check_host_trust(request.environ):
  330. return SecurityError() # type: ignore[return-value]
  331. if 0 not in self.frames:
  332. if self.console_init_func is None:
  333. ns = {}
  334. else:
  335. ns = dict(self.console_init_func())
  336. ns.setdefault("app", self.app)
  337. self.frames[0] = _ConsoleFrame(ns)
  338. is_trusted = bool(self.check_pin_trust(request.environ))
  339. return Response(
  340. render_console_html(secret=self.secret, evalex_trusted=is_trusted),
  341. mimetype="text/html",
  342. )
  343. def get_resource(self, request: Request, filename: str) -> Response:
  344. """Return a static resource from the shared folder."""
  345. path = join("shared", basename(filename))
  346. try:
  347. data = pkgutil.get_data(__package__, path)
  348. except OSError:
  349. return NotFound() # type: ignore[return-value]
  350. else:
  351. if data is None:
  352. return NotFound() # type: ignore[return-value]
  353. etag = str(adler32(data) & 0xFFFFFFFF)
  354. return send_file(
  355. BytesIO(data), request.environ, download_name=filename, etag=etag
  356. )
  357. def check_pin_trust(self, environ: WSGIEnvironment) -> bool | None:
  358. """Checks if the request passed the pin test. This returns `True` if the
  359. request is trusted on a pin/cookie basis and returns `False` if not.
  360. Additionally if the cookie's stored pin hash is wrong it will return
  361. `None` so that appropriate action can be taken.
  362. """
  363. if self.pin is None:
  364. return True
  365. val = parse_cookie(environ).get(self.pin_cookie_name)
  366. if not val or "|" not in val:
  367. return False
  368. ts_str, pin_hash = val.split("|", 1)
  369. try:
  370. ts = int(ts_str)
  371. except ValueError:
  372. return False
  373. if pin_hash != hash_pin(self.pin):
  374. return None
  375. return (time.time() - PIN_TIME) < ts
  376. def check_host_trust(self, environ: WSGIEnvironment) -> bool:
  377. return host_is_trusted(environ.get("HTTP_HOST"), self.trusted_hosts)
  378. def _fail_pin_auth(self) -> None:
  379. with self._failed_pin_auth.get_lock():
  380. count = self._failed_pin_auth.value
  381. self._failed_pin_auth.value = count + 1
  382. time.sleep(5.0 if count > 5 else 0.5)
  383. def pin_auth(self, request: Request) -> Response:
  384. """Authenticates with the pin."""
  385. if not self.check_host_trust(request.environ):
  386. return SecurityError() # type: ignore[return-value]
  387. exhausted = False
  388. auth = False
  389. trust = self.check_pin_trust(request.environ)
  390. pin = t.cast(str, self.pin)
  391. # If the trust return value is `None` it means that the cookie is
  392. # set but the stored pin hash value is bad. This means that the
  393. # pin was changed. In this case we count a bad auth and unset the
  394. # cookie. This way it becomes harder to guess the cookie name
  395. # instead of the pin as we still count up failures.
  396. bad_cookie = False
  397. if trust is None:
  398. self._fail_pin_auth()
  399. bad_cookie = True
  400. # If we're trusted, we're authenticated.
  401. elif trust:
  402. auth = True
  403. # If we failed too many times, then we're locked out.
  404. elif self._failed_pin_auth.value > 10:
  405. exhausted = True
  406. # Otherwise go through pin based authentication
  407. else:
  408. entered_pin = request.args["pin"]
  409. if entered_pin.strip().replace("-", "") == pin.replace("-", ""):
  410. self._failed_pin_auth.value = 0
  411. auth = True
  412. else:
  413. self._fail_pin_auth()
  414. rv = Response(
  415. json.dumps({"auth": auth, "exhausted": exhausted}),
  416. mimetype="application/json",
  417. )
  418. if auth:
  419. rv.set_cookie(
  420. self.pin_cookie_name,
  421. f"{int(time.time())}|{hash_pin(pin)}",
  422. httponly=True,
  423. samesite="Strict",
  424. secure=request.is_secure,
  425. )
  426. elif bad_cookie:
  427. rv.delete_cookie(self.pin_cookie_name)
  428. return rv
  429. def log_pin_request(self, request: Request) -> Response:
  430. """Log the pin if needed."""
  431. if not self.check_host_trust(request.environ):
  432. return SecurityError() # type: ignore[return-value]
  433. if self.pin_logging and self.pin is not None:
  434. _log(
  435. "info", " * To enable the debugger you need to enter the security pin:"
  436. )
  437. _log("info", " * Debugger pin code: %s", self.pin)
  438. return Response("")
  439. def __call__(
  440. self, environ: WSGIEnvironment, start_response: StartResponse
  441. ) -> t.Iterable[bytes]:
  442. """Dispatch the requests."""
  443. # important: don't ever access a function here that reads the incoming
  444. # form data! Otherwise the application won't have access to that data
  445. # any more!
  446. request = Request(environ)
  447. response = self.debug_application
  448. if request.args.get("__debugger__") == "yes":
  449. cmd = request.args.get("cmd")
  450. arg = request.args.get("f")
  451. secret = request.args.get("s")
  452. frame = self.frames.get(request.args.get("frm", type=int)) # type: ignore
  453. if cmd == "resource" and arg:
  454. response = self.get_resource(request, arg) # type: ignore
  455. elif cmd == "pinauth" and secret == self.secret:
  456. response = self.pin_auth(request) # type: ignore
  457. elif cmd == "printpin" and secret == self.secret:
  458. response = self.log_pin_request(request) # type: ignore
  459. elif (
  460. self.evalex
  461. and cmd is not None
  462. and frame is not None
  463. and self.secret == secret
  464. and self.check_pin_trust(environ)
  465. ):
  466. response = self.execute_command(request, cmd, frame) # type: ignore
  467. elif (
  468. self.evalex
  469. and self.console_path is not None
  470. and request.path == self.console_path
  471. ):
  472. response = self.display_console(request) # type: ignore
  473. return response(environ, start_response)