proxy_fix.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. """
  2. X-Forwarded-For Proxy Fix
  3. =========================
  4. This module provides a middleware that adjusts the WSGI environ based on
  5. ``X-Forwarded-`` headers that proxies in front of an application may
  6. set.
  7. When an application is running behind a proxy server, WSGI may see the
  8. request as coming from that server rather than the real client. Proxies
  9. set various headers to track where the request actually came from.
  10. This middleware should only be used if the application is actually
  11. behind such a proxy, and should be configured with the number of proxies
  12. that are chained in front of it. Not all proxies set all the headers.
  13. Since incoming headers can be faked, you must set how many proxies are
  14. setting each header so the middleware knows what to trust.
  15. .. autoclass:: ProxyFix
  16. :copyright: 2007 Pallets
  17. :license: BSD-3-Clause
  18. """
  19. import typing as t
  20. from ..http import parse_list_header
  21. if t.TYPE_CHECKING:
  22. from _typeshed.wsgi import StartResponse
  23. from _typeshed.wsgi import WSGIApplication
  24. from _typeshed.wsgi import WSGIEnvironment
  25. class ProxyFix:
  26. """Adjust the WSGI environ based on ``X-Forwarded-`` that proxies in
  27. front of the application may set.
  28. - ``X-Forwarded-For`` sets ``REMOTE_ADDR``.
  29. - ``X-Forwarded-Proto`` sets ``wsgi.url_scheme``.
  30. - ``X-Forwarded-Host`` sets ``HTTP_HOST``, ``SERVER_NAME``, and
  31. ``SERVER_PORT``.
  32. - ``X-Forwarded-Port`` sets ``HTTP_HOST`` and ``SERVER_PORT``.
  33. - ``X-Forwarded-Prefix`` sets ``SCRIPT_NAME``.
  34. You must tell the middleware how many proxies set each header so it
  35. knows what values to trust. It is a security issue to trust values
  36. that came from the client rather than a proxy.
  37. The original values of the headers are stored in the WSGI
  38. environ as ``werkzeug.proxy_fix.orig``, a dict.
  39. :param app: The WSGI application to wrap.
  40. :param x_for: Number of values to trust for ``X-Forwarded-For``.
  41. :param x_proto: Number of values to trust for ``X-Forwarded-Proto``.
  42. :param x_host: Number of values to trust for ``X-Forwarded-Host``.
  43. :param x_port: Number of values to trust for ``X-Forwarded-Port``.
  44. :param x_prefix: Number of values to trust for
  45. ``X-Forwarded-Prefix``.
  46. .. code-block:: python
  47. from werkzeug.middleware.proxy_fix import ProxyFix
  48. # App is behind one proxy that sets the -For and -Host headers.
  49. app = ProxyFix(app, x_for=1, x_host=1)
  50. .. versionchanged:: 1.0
  51. Deprecated code has been removed:
  52. * The ``num_proxies`` argument and attribute.
  53. * The ``get_remote_addr`` method.
  54. * The environ keys ``orig_remote_addr``,
  55. ``orig_wsgi_url_scheme``, and ``orig_http_host``.
  56. .. versionchanged:: 0.15
  57. All headers support multiple values. The ``num_proxies``
  58. argument is deprecated. Each header is configured with a
  59. separate number of trusted proxies.
  60. .. versionchanged:: 0.15
  61. Original WSGI environ values are stored in the
  62. ``werkzeug.proxy_fix.orig`` dict. ``orig_remote_addr``,
  63. ``orig_wsgi_url_scheme``, and ``orig_http_host`` are deprecated
  64. and will be removed in 1.0.
  65. .. versionchanged:: 0.15
  66. Support ``X-Forwarded-Port`` and ``X-Forwarded-Prefix``.
  67. .. versionchanged:: 0.15
  68. ``X-Forwarded-Host`` and ``X-Forwarded-Port`` modify
  69. ``SERVER_NAME`` and ``SERVER_PORT``.
  70. """
  71. def __init__(
  72. self,
  73. app: "WSGIApplication",
  74. x_for: int = 1,
  75. x_proto: int = 1,
  76. x_host: int = 0,
  77. x_port: int = 0,
  78. x_prefix: int = 0,
  79. ) -> None:
  80. self.app = app
  81. self.x_for = x_for
  82. self.x_proto = x_proto
  83. self.x_host = x_host
  84. self.x_port = x_port
  85. self.x_prefix = x_prefix
  86. def _get_real_value(self, trusted: int, value: t.Optional[str]) -> t.Optional[str]:
  87. """Get the real value from a list header based on the configured
  88. number of trusted proxies.
  89. :param trusted: Number of values to trust in the header.
  90. :param value: Comma separated list header value to parse.
  91. :return: The real value, or ``None`` if there are fewer values
  92. than the number of trusted proxies.
  93. .. versionchanged:: 1.0
  94. Renamed from ``_get_trusted_comma``.
  95. .. versionadded:: 0.15
  96. """
  97. if not (trusted and value):
  98. return None
  99. values = parse_list_header(value)
  100. if len(values) >= trusted:
  101. return values[-trusted]
  102. return None
  103. def __call__(
  104. self, environ: "WSGIEnvironment", start_response: "StartResponse"
  105. ) -> t.Iterable[bytes]:
  106. """Modify the WSGI environ based on the various ``Forwarded``
  107. headers before calling the wrapped application. Store the
  108. original environ values in ``werkzeug.proxy_fix.orig_{key}``.
  109. """
  110. environ_get = environ.get
  111. orig_remote_addr = environ_get("REMOTE_ADDR")
  112. orig_wsgi_url_scheme = environ_get("wsgi.url_scheme")
  113. orig_http_host = environ_get("HTTP_HOST")
  114. environ.update(
  115. {
  116. "werkzeug.proxy_fix.orig": {
  117. "REMOTE_ADDR": orig_remote_addr,
  118. "wsgi.url_scheme": orig_wsgi_url_scheme,
  119. "HTTP_HOST": orig_http_host,
  120. "SERVER_NAME": environ_get("SERVER_NAME"),
  121. "SERVER_PORT": environ_get("SERVER_PORT"),
  122. "SCRIPT_NAME": environ_get("SCRIPT_NAME"),
  123. }
  124. }
  125. )
  126. x_for = self._get_real_value(self.x_for, environ_get("HTTP_X_FORWARDED_FOR"))
  127. if x_for:
  128. environ["REMOTE_ADDR"] = x_for
  129. x_proto = self._get_real_value(
  130. self.x_proto, environ_get("HTTP_X_FORWARDED_PROTO")
  131. )
  132. if x_proto:
  133. environ["wsgi.url_scheme"] = x_proto
  134. x_host = self._get_real_value(self.x_host, environ_get("HTTP_X_FORWARDED_HOST"))
  135. if x_host:
  136. environ["HTTP_HOST"] = environ["SERVER_NAME"] = x_host
  137. # "]" to check for IPv6 address without port
  138. if ":" in x_host and not x_host.endswith("]"):
  139. environ["SERVER_NAME"], environ["SERVER_PORT"] = x_host.rsplit(":", 1)
  140. x_port = self._get_real_value(self.x_port, environ_get("HTTP_X_FORWARDED_PORT"))
  141. if x_port:
  142. host = environ.get("HTTP_HOST")
  143. if host:
  144. # "]" to check for IPv6 address without port
  145. if ":" in host and not host.endswith("]"):
  146. host = host.rsplit(":", 1)[0]
  147. environ["HTTP_HOST"] = f"{host}:{x_port}"
  148. environ["SERVER_PORT"] = x_port
  149. x_prefix = self._get_real_value(
  150. self.x_prefix, environ_get("HTTP_X_FORWARDED_PREFIX")
  151. )
  152. if x_prefix:
  153. environ["SCRIPT_NAME"] = x_prefix
  154. return self.app(environ, start_response)