__init__.py 18 KB

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