-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add support for X-Forwarded-Proto #9472
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add support for `X-Forwarded-Proto` header when using a reverse proxy. Administrators using a reverse proxy should ensure this header is set to avoid warnings. See [docs/workers.md](docs/workers.md) for example configurations. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,10 @@ | |
import time | ||
from typing import Optional, Union | ||
|
||
import attr | ||
from zope.interface import implementer | ||
|
||
from twisted.internet.interfaces import IAddress | ||
from twisted.python.failure import Failure | ||
from twisted.web.server import Request, Site | ||
|
||
|
@@ -333,26 +337,77 @@ def _should_log_request(self) -> bool: | |
|
||
|
||
class XForwardedForRequest(SynapseRequest): | ||
def __init__(self, *args, **kw): | ||
SynapseRequest.__init__(self, *args, **kw) | ||
"""Request object which honours proxy headers | ||
|
||
Extends SynapseRequest to replace getClientIP, getClientAddress, and isSecure with | ||
information from request headers. | ||
""" | ||
Add a layer on top of another request that only uses the value of an | ||
X-Forwarded-For header as the result of C{getClientIP}. | ||
""" | ||
|
||
def getClientIP(self): | ||
# the client IP and ssl flag, as extracted from the headers. | ||
_forwarded_for = None # type: Optional[_XForwardedForAddress] | ||
_forwarded_https = False # type: bool | ||
|
||
def requestReceived(self, command, path, version): | ||
# this method is called by the Channel once the full request has been | ||
# received, to dispatch the request to a resource. | ||
# We can use it to set the IP address and protocol according to the | ||
# headers. | ||
self._process_forwarded_headers() | ||
return super().requestReceived(command, path, version) | ||
|
||
def _process_forwarded_headers(self): | ||
headers = self.requestHeaders.getRawHeaders(b"x-forwarded-for") | ||
if not headers: | ||
return | ||
|
||
# for now, we just use the first x-forwarded-for header. Really, we ought | ||
# to start from the client IP address, and check whether it is trusted; if it | ||
# is, work backwards through the headers until we find an untrusted address. | ||
# see https://github.com/matrix-org/synapse/issues/9471 | ||
self._forwarded_for = _XForwardedForAddress( | ||
headers[0].split(b",")[0].strip().decode("ascii") | ||
) | ||
|
||
# if we got an x-forwarded-for header, also look for an x-forwarded-proto header | ||
header = self.getHeader(b"x-forwarded-proto") | ||
if header is not None: | ||
self._forwarded_https = header.lower() == b"https" | ||
else: | ||
# this is done largely for backwards-compatibility so that people that | ||
# haven't set an x-forwarded-proto header don't get a redirect loop. | ||
logger.warning( | ||
"forwarded request lacks an x-forwarded-proto header: assuming https" | ||
) | ||
self._forwarded_https = True | ||
|
||
def isSecure(self): | ||
if self._forwarded_https: | ||
return True | ||
return super().isSecure() | ||
|
||
def getClientIP(self) -> str: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It seems that this gets used to fill the This is a misconfiguration, but is a change in behavior so thought it was worth calling out. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, that behaviour always seemed a bit magical to me. Why Note that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My point was more of that a private IP could potentially be exposed to users who are not system administrators. (This is what I meant by public.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, yes. Well, I feel like it's a fairly basic configuration error. Don't do that. |
||
""" | ||
@return: The client address (the first address) in the value of the | ||
I{X-Forwarded-For header}. If the header is not present, return | ||
C{b"-"}. | ||
Return the IP address of the client who submitted this request. | ||
|
||
This method is deprecated. Use getClientAddress() instead. | ||
""" | ||
return ( | ||
self.requestHeaders.getRawHeaders(b"x-forwarded-for", [b"-"])[0] | ||
.split(b",")[0] | ||
.strip() | ||
.decode("ascii") | ||
) | ||
if self._forwarded_for is not None: | ||
return self._forwarded_for.host | ||
return super().getClientIP() | ||
|
||
def getClientAddress(self) -> IAddress: | ||
""" | ||
Return the address of the client who submitted this request. | ||
""" | ||
if self._forwarded_for is not None: | ||
return self._forwarded_for | ||
return super().getClientAddress() | ||
|
||
|
||
@implementer(IAddress) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've always found this interface bizarre that it has no fields... |
||
@attr.s(frozen=True, slots=True) | ||
class _XForwardedForAddress: | ||
host = attr.ib(type=str) | ||
|
||
|
||
class SynapseSite(Site): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apache and caddy both set X-Forwarded-For by default, though in a way that #9471 is problematic.