__init__.py 18 KB

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