proxy_headers.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. from collections import namedtuple
  2. from .utilities import BadRequest, logger, undquote
  3. PROXY_HEADERS = frozenset(
  4. {
  5. "X_FORWARDED_FOR",
  6. "X_FORWARDED_HOST",
  7. "X_FORWARDED_PROTO",
  8. "X_FORWARDED_PORT",
  9. "X_FORWARDED_BY",
  10. "FORWARDED",
  11. }
  12. )
  13. Forwarded = namedtuple("Forwarded", ["by", "for_", "host", "proto"])
  14. class MalformedProxyHeader(Exception):
  15. def __init__(self, header, reason, value):
  16. self.header = header
  17. self.reason = reason
  18. self.value = value
  19. super().__init__(header, reason, value)
  20. def proxy_headers_middleware(
  21. app,
  22. trusted_proxy=None,
  23. trusted_proxy_count=1,
  24. trusted_proxy_headers=None,
  25. clear_untrusted=True,
  26. log_untrusted=False,
  27. logger=logger,
  28. ):
  29. def translate_proxy_headers(environ, start_response):
  30. untrusted_headers = PROXY_HEADERS
  31. remote_peer = environ["REMOTE_ADDR"]
  32. if trusted_proxy == "*" or remote_peer == trusted_proxy:
  33. try:
  34. untrusted_headers = parse_proxy_headers(
  35. environ,
  36. trusted_proxy_count=trusted_proxy_count,
  37. trusted_proxy_headers=trusted_proxy_headers,
  38. logger=logger,
  39. )
  40. except MalformedProxyHeader as ex:
  41. logger.warning(
  42. 'Malformed proxy header "%s" from "%s": %s value: %s',
  43. ex.header,
  44. remote_peer,
  45. ex.reason,
  46. ex.value,
  47. )
  48. error = BadRequest(f'Header "{ex.header}" malformed.')
  49. return error.wsgi_response(environ, start_response)
  50. # Clear out the untrusted proxy headers
  51. if clear_untrusted:
  52. clear_untrusted_headers(
  53. environ, untrusted_headers, log_warning=log_untrusted, logger=logger
  54. )
  55. return app(environ, start_response)
  56. return translate_proxy_headers
  57. def parse_proxy_headers(
  58. environ, trusted_proxy_count, trusted_proxy_headers, logger=logger
  59. ):
  60. if trusted_proxy_headers is None:
  61. trusted_proxy_headers = set()
  62. forwarded_for = []
  63. forwarded_host = forwarded_proto = forwarded_port = forwarded = ""
  64. client_addr = None
  65. untrusted_headers = set(PROXY_HEADERS)
  66. def raise_for_multiple_values():
  67. raise ValueError("Unspecified behavior for multiple values found in header")
  68. if "x-forwarded-for" in trusted_proxy_headers and "HTTP_X_FORWARDED_FOR" in environ:
  69. try:
  70. forwarded_for = []
  71. for forward_hop in environ["HTTP_X_FORWARDED_FOR"].split(","):
  72. forward_hop = forward_hop.strip()
  73. forward_hop = undquote(forward_hop)
  74. # Make sure that all IPv6 addresses are surrounded by brackets,
  75. # this is assuming that the IPv6 representation here does not
  76. # include a port number.
  77. if "." not in forward_hop and (
  78. ":" in forward_hop and forward_hop[-1] != "]"
  79. ):
  80. forwarded_for.append(f"[{forward_hop}]")
  81. else:
  82. forwarded_for.append(forward_hop)
  83. forwarded_for = forwarded_for[-trusted_proxy_count:]
  84. client_addr = forwarded_for[0]
  85. untrusted_headers.remove("X_FORWARDED_FOR")
  86. except Exception as ex:
  87. raise MalformedProxyHeader(
  88. "X-Forwarded-For", str(ex), environ["HTTP_X_FORWARDED_FOR"]
  89. )
  90. if (
  91. "x-forwarded-host" in trusted_proxy_headers
  92. and "HTTP_X_FORWARDED_HOST" in environ
  93. ):
  94. try:
  95. forwarded_host_multiple = []
  96. for forward_host in environ["HTTP_X_FORWARDED_HOST"].split(","):
  97. forward_host = forward_host.strip()
  98. forward_host = undquote(forward_host)
  99. forwarded_host_multiple.append(forward_host)
  100. forwarded_host_multiple = forwarded_host_multiple[-trusted_proxy_count:]
  101. forwarded_host = forwarded_host_multiple[0]
  102. untrusted_headers.remove("X_FORWARDED_HOST")
  103. except Exception as ex:
  104. raise MalformedProxyHeader(
  105. "X-Forwarded-Host", str(ex), environ["HTTP_X_FORWARDED_HOST"]
  106. )
  107. if "x-forwarded-proto" in trusted_proxy_headers:
  108. try:
  109. forwarded_proto = undquote(environ.get("HTTP_X_FORWARDED_PROTO", ""))
  110. if "," in forwarded_proto:
  111. raise_for_multiple_values()
  112. untrusted_headers.remove("X_FORWARDED_PROTO")
  113. except Exception as ex:
  114. raise MalformedProxyHeader(
  115. "X-Forwarded-Proto", str(ex), environ["HTTP_X_FORWARDED_PROTO"]
  116. )
  117. if "x-forwarded-port" in trusted_proxy_headers:
  118. try:
  119. forwarded_port = undquote(environ.get("HTTP_X_FORWARDED_PORT", ""))
  120. if "," in forwarded_port:
  121. raise_for_multiple_values()
  122. untrusted_headers.remove("X_FORWARDED_PORT")
  123. except Exception as ex:
  124. raise MalformedProxyHeader(
  125. "X-Forwarded-Port", str(ex), environ["HTTP_X_FORWARDED_PORT"]
  126. )
  127. if "x-forwarded-by" in trusted_proxy_headers:
  128. # Waitress itself does not use X-Forwarded-By, but we can not
  129. # remove it so it can get set in the environ
  130. untrusted_headers.remove("X_FORWARDED_BY")
  131. if "forwarded" in trusted_proxy_headers:
  132. forwarded = environ.get("HTTP_FORWARDED", None)
  133. untrusted_headers = PROXY_HEADERS - {"FORWARDED"}
  134. # If the Forwarded header exists, it gets priority
  135. if forwarded:
  136. proxies = []
  137. try:
  138. for forwarded_element in forwarded.split(","):
  139. # Remove whitespace that may have been introduced when
  140. # appending a new entry
  141. forwarded_element = forwarded_element.strip()
  142. forwarded_for = forwarded_host = forwarded_proto = ""
  143. forwarded_port = forwarded_by = ""
  144. for pair in forwarded_element.split(";"):
  145. pair = pair.lower()
  146. if not pair:
  147. continue
  148. token, equals, value = pair.partition("=")
  149. if equals != "=":
  150. raise ValueError('Invalid forwarded-pair missing "="')
  151. if token.strip() != token:
  152. raise ValueError("Token may not be surrounded by whitespace")
  153. if value.strip() != value:
  154. raise ValueError("Value may not be surrounded by whitespace")
  155. if token == "by":
  156. forwarded_by = undquote(value)
  157. elif token == "for":
  158. forwarded_for = undquote(value)
  159. elif token == "host":
  160. forwarded_host = undquote(value)
  161. elif token == "proto":
  162. forwarded_proto = undquote(value)
  163. else:
  164. logger.warning("Unknown Forwarded token: %s" % token)
  165. proxies.append(
  166. Forwarded(
  167. forwarded_by, forwarded_for, forwarded_host, forwarded_proto
  168. )
  169. )
  170. except Exception as ex:
  171. raise MalformedProxyHeader("Forwarded", str(ex), environ["HTTP_FORWARDED"])
  172. proxies = proxies[-trusted_proxy_count:]
  173. # Iterate backwards and fill in some values, the oldest entry that
  174. # contains the information we expect is the one we use. We expect
  175. # that intermediate proxies may re-write the host header or proto,
  176. # but the oldest entry is the one that contains the information the
  177. # client expects when generating URL's
  178. #
  179. # Forwarded: for="[2001:db8::1]";host="example.com:8443";proto="https"
  180. # Forwarded: for=192.0.2.1;host="example.internal:8080"
  181. #
  182. # (After HTTPS header folding) should mean that we use as values:
  183. #
  184. # Host: example.com
  185. # Protocol: https
  186. # Port: 8443
  187. for proxy in proxies[::-1]:
  188. client_addr = proxy.for_ or client_addr
  189. forwarded_host = proxy.host or forwarded_host
  190. forwarded_proto = proxy.proto or forwarded_proto
  191. if forwarded_proto:
  192. forwarded_proto = forwarded_proto.lower()
  193. if forwarded_proto not in {"http", "https"}:
  194. raise MalformedProxyHeader(
  195. "Forwarded Proto=" if forwarded else "X-Forwarded-Proto",
  196. "unsupported proto value",
  197. forwarded_proto,
  198. )
  199. # Set the URL scheme to the proxy provided proto
  200. environ["wsgi.url_scheme"] = forwarded_proto
  201. if not forwarded_port:
  202. if forwarded_proto == "http":
  203. forwarded_port = "80"
  204. if forwarded_proto == "https":
  205. forwarded_port = "443"
  206. if forwarded_host:
  207. if ":" in forwarded_host and forwarded_host[-1] != "]":
  208. host, port = forwarded_host.rsplit(":", 1)
  209. host, port = host.strip(), str(port)
  210. # We trust the port in the Forwarded Host/X-Forwarded-Host over
  211. # X-Forwarded-Port, or whatever we got from Forwarded
  212. # Proto/X-Forwarded-Proto.
  213. if forwarded_port != port:
  214. forwarded_port = port
  215. # We trust the proxy server's forwarded Host
  216. environ["SERVER_NAME"] = host
  217. environ["HTTP_HOST"] = forwarded_host
  218. else:
  219. # We trust the proxy server's forwarded Host
  220. environ["SERVER_NAME"] = forwarded_host
  221. environ["HTTP_HOST"] = forwarded_host
  222. if forwarded_port:
  223. if forwarded_port not in {"443", "80"}:
  224. environ["HTTP_HOST"] = "{}:{}".format(
  225. forwarded_host, forwarded_port
  226. )
  227. elif forwarded_port == "80" and environ["wsgi.url_scheme"] != "http":
  228. environ["HTTP_HOST"] = "{}:{}".format(
  229. forwarded_host, forwarded_port
  230. )
  231. elif forwarded_port == "443" and environ["wsgi.url_scheme"] != "https":
  232. environ["HTTP_HOST"] = "{}:{}".format(
  233. forwarded_host, forwarded_port
  234. )
  235. if forwarded_port:
  236. environ["SERVER_PORT"] = str(forwarded_port)
  237. if client_addr:
  238. if ":" in client_addr and client_addr[-1] != "]":
  239. addr, port = client_addr.rsplit(":", 1)
  240. environ["REMOTE_ADDR"] = strip_brackets(addr.strip())
  241. environ["REMOTE_PORT"] = port.strip()
  242. else:
  243. environ["REMOTE_ADDR"] = strip_brackets(client_addr.strip())
  244. environ["REMOTE_HOST"] = environ["REMOTE_ADDR"]
  245. return untrusted_headers
  246. def strip_brackets(addr):
  247. if addr[0] == "[" and addr[-1] == "]":
  248. return addr[1:-1]
  249. return addr
  250. def clear_untrusted_headers(
  251. environ, untrusted_headers, log_warning=False, logger=logger
  252. ):
  253. untrusted_headers_removed = [
  254. header
  255. for header in untrusted_headers
  256. if environ.pop("HTTP_" + header, False) is not False
  257. ]
  258. if log_warning and untrusted_headers_removed:
  259. untrusted_headers_removed = [
  260. "-".join(x.capitalize() for x in header.split("_"))
  261. for header in untrusted_headers_removed
  262. ]
  263. logger.warning(
  264. "Removed untrusted headers (%s). Waitress recommends these be "
  265. "removed upstream.",
  266. ", ".join(untrusted_headers_removed),
  267. )