summary refs log tree commit diff
path: root/synapse/http/server.py
diff options
context:
space:
mode:
authorPatrick Cloke <patrickc@matrix.org>2020-07-02 10:54:29 -0400
committerPatrick Cloke <patrickc@matrix.org>2020-07-02 10:54:29 -0400
commitfedb632d0a22e4bad179a9b48be28a3a552315c9 (patch)
tree9f05e09e55851f8e272a5e1f722472f5aef04050 /synapse/http/server.py
parentUpdate postgres in the Docker compose example to 12-alpine. (#7696) (diff)
parentRemove an extraneous space. (diff)
downloadsynapse-fedb632d0a22e4bad179a9b48be28a3a552315c9.tar.xz
Merge tag 'v1.15.2'
Synapse 1.15.2 (2020-07-02)
===========================

Due to the two security issues highlighted below, server administrators are
encouraged to update Synapse. We are not aware of these vulnerabilities being
exploited in the wild.

Security advisory
-----------------

* A malicious homeserver could force Synapse to reset the state in a room to a
  small subset of the correct state. This affects all Synapse deployments which
  federate with untrusted servers. ([96e9afe6](https://github.com/matrix-org/synapse/commit/96e9afe62500310977dc3cbc99a8d16d3d2fa15c))
* HTML pages served via Synapse were vulnerable to clickjacking attacks. This
  predominantly affects homeservers with single-sign-on enabled, but all server
  administrators are encouraged to upgrade. ([ea26e9a9](https://github.com/matrix-org/synapse/commit/ea26e9a98b0541fc886a1cb826a38352b7599dbe))

  This was reported by [Quentin Gliech](https://sandhose.fr/).
Diffstat (limited to 'synapse/http/server.py')
-rw-r--r--synapse/http/server.py76
1 files changed, 68 insertions, 8 deletions
diff --git a/synapse/http/server.py b/synapse/http/server.py
index 2487a72171..2331a2a4b0 100644
--- a/synapse/http/server.py
+++ b/synapse/http/server.py
@@ -30,7 +30,7 @@ from twisted.internet import defer
 from twisted.python import failure
 from twisted.web import resource
 from twisted.web.server import NOT_DONE_YET, Request
-from twisted.web.static import NoRangeStaticProducer
+from twisted.web.static import File, NoRangeStaticProducer
 from twisted.web.util import redirectTo
 
 import synapse.events
@@ -202,12 +202,7 @@ def return_html_error(
     else:
         body = error_template.render(code=code, msg=msg)
 
-    body_bytes = body.encode("utf-8")
-    request.setResponseCode(code)
-    request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
-    request.setHeader(b"Content-Length", b"%i" % (len(body_bytes),))
-    request.write(body_bytes)
-    finish_request(request)
+    respond_with_html(request, code, body)
 
 
 def wrap_async_request_handler(h):
@@ -420,6 +415,18 @@ class DirectServeResource(resource.Resource):
         return NOT_DONE_YET
 
 
+class StaticResource(File):
+    """
+    A resource that represents a plain non-interpreted file or directory.
+
+    Differs from the File resource by adding clickjacking protection.
+    """
+
+    def render_GET(self, request: Request):
+        set_clickjacking_protection_headers(request)
+        return super().render_GET(request)
+
+
 def _options_handler(request):
     """Request handler for OPTIONS requests
 
@@ -530,7 +537,7 @@ def respond_with_json_bytes(
         code (int): The HTTP response code.
         json_bytes (bytes): The json bytes to use as the response body.
         send_cors (bool): Whether to send Cross-Origin Resource Sharing headers
-            http://www.w3.org/TR/cors/
+            https://fetch.spec.whatwg.org/#http-cors-protocol
     Returns:
         twisted.web.server.NOT_DONE_YET"""
 
@@ -568,6 +575,59 @@ def set_cors_headers(request):
     )
 
 
+def respond_with_html(request: Request, code: int, html: str):
+    """
+    Wraps `respond_with_html_bytes` by first encoding HTML from a str to UTF-8 bytes.
+    """
+    respond_with_html_bytes(request, code, html.encode("utf-8"))
+
+
+def respond_with_html_bytes(request: Request, code: int, html_bytes: bytes):
+    """
+    Sends HTML (encoded as UTF-8 bytes) as the response to the given request.
+
+    Note that this adds clickjacking protection headers and finishes the request.
+
+    Args:
+        request: The http request to respond to.
+        code: The HTTP response code.
+        html_bytes: The HTML bytes to use as the response body.
+    """
+    # could alternatively use request.notifyFinish() and flip a flag when
+    # the Deferred fires, but since the flag is RIGHT THERE it seems like
+    # a waste.
+    if request._disconnected:
+        logger.warning(
+            "Not sending response to request %s, already disconnected.", request
+        )
+        return
+
+    request.setResponseCode(code)
+    request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
+    request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
+
+    # Ensure this content cannot be embedded.
+    set_clickjacking_protection_headers(request)
+
+    request.write(html_bytes)
+    finish_request(request)
+
+
+def set_clickjacking_protection_headers(request: Request):
+    """
+    Set headers to guard against clickjacking of embedded content.
+
+    This sets the X-Frame-Options and Content-Security-Policy headers which instructs
+    browsers to not allow the HTML of the response to be embedded onto another
+    page.
+
+    Args:
+        request: The http request to add the headers to.
+    """
+    request.setHeader(b"X-Frame-Options", b"DENY")
+    request.setHeader(b"Content-Security-Policy", b"frame-ancestors 'none';")
+
+
 def finish_request(request):
     """ Finish writing the response to the request.