Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Ensure that HTML pages served from Synapse include headers to avoid e…
Browse files Browse the repository at this point in the history
…mbedding.
  • Loading branch information
clokep committed Jul 2, 2020
1 parent 0fc5575 commit ea26e9a
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 94 deletions.
3 changes: 2 additions & 1 deletion synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
OptionsResource,
RootOptionsRedirectResource,
RootRedirect,
StaticResource,
)
from synapse.http.site import SynapseSite
from synapse.logging.context import LoggingContext
Expand Down Expand Up @@ -228,7 +229,7 @@ def _configure_named_resource(self, name, compress=False):
if name in ["static", "client"]:
resources.update(
{
STATIC_PREFIX: File(
STATIC_PREFIX: StaticResource(
os.path.join(os.path.dirname(synapse.__file__), "static")
)
}
Expand Down
30 changes: 7 additions & 23 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from synapse.api.ratelimiting import Ratelimiter
from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
from synapse.http.server import finish_request
from synapse.http.server import finish_request, respond_with_html
from synapse.http.site import SynapseRequest
from synapse.logging.context import defer_to_thread
from synapse.metrics.background_process_metrics import run_as_background_process
Expand Down Expand Up @@ -1055,13 +1055,8 @@ async def complete_sso_ui_auth(
)

# Render the HTML and return.
html_bytes = self._sso_auth_success_template.encode("utf-8")
request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

request.write(html_bytes)
finish_request(request)
html = self._sso_auth_success_template
respond_with_html(request, 200, html)

async def complete_sso_login(
self,
Expand All @@ -1081,13 +1076,7 @@ async def complete_sso_login(
# flow.
deactivated = await self.store.get_user_deactivated_status(registered_user_id)
if deactivated:
html_bytes = self._sso_account_deactivated_template.encode("utf-8")

request.setResponseCode(403)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
request.write(html_bytes)
finish_request(request)
respond_with_html(request, 403, self._sso_account_deactivated_template)
return

self._complete_sso_login(registered_user_id, request, client_redirect_url)
Expand Down Expand Up @@ -1128,17 +1117,12 @@ def _complete_sso_login(
# URL we redirect users to.
redirect_url_no_params = client_redirect_url.split("?")[0]

html_bytes = self._sso_redirect_confirm_template.render(
html = self._sso_redirect_confirm_template.render(
display_url=redirect_url_no_params,
redirect_url=redirect_url,
server_name=self._server_name,
).encode("utf-8")

request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
request.write(html_bytes)
finish_request(request)
)
respond_with_html(request, 200, html)

@staticmethod
def add_query_param_to_url(url: str, param_name: str, param: Any):
Expand Down
13 changes: 4 additions & 9 deletions synapse/handlers/oidc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from twisted.web.client import readBody

from synapse.config import ConfigError
from synapse.http.server import finish_request
from synapse.http.server import respond_with_html
from synapse.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable
from synapse.push.mailer import load_jinja2_templates
Expand Down Expand Up @@ -144,15 +144,10 @@ def _render_error(
access_denied.
error_description: A human-readable description of the error.
"""
html_bytes = self._error_template.render(
html = self._error_template.render(
error=error, error_description=error_description
).encode("utf-8")

request.setResponseCode(400)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%i" % len(html_bytes))
request.write(html_bytes)
finish_request(request)
)
respond_with_html(request, 400, html)

def _validate_metadata(self):
"""Verifies the provider metadata.
Expand Down
76 changes: 68 additions & 8 deletions synapse/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -420,6 +415,18 @@ def render(self, request):
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
Expand Down Expand Up @@ -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"""

Expand Down Expand Up @@ -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.
Expand Down
10 changes: 3 additions & 7 deletions synapse/rest/client/v1/pusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import logging

from synapse.api.errors import Codes, StoreError, SynapseError
from synapse.http.server import finish_request
from synapse.http.server import respond_with_html_bytes
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
Expand Down Expand Up @@ -177,13 +177,9 @@ async def on_GET(self, request):

self.notifier.on_new_replication_data()

request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(
b"Content-Length", b"%d" % (len(PushersRemoveRestServlet.SUCCESS_HTML),)
respond_with_html_bytes(
request, 200, PushersRemoveRestServlet.SUCCESS_HTML,
)
request.write(PushersRemoveRestServlet.SUCCESS_HTML)
finish_request(request)
return None

def on_OPTIONS(self, _):
Expand Down
16 changes: 7 additions & 9 deletions synapse/rest/client/v2_alpha/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from synapse.api.constants import LoginType
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.http.server import finish_request
from synapse.http.server import finish_request, respond_with_html
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
Expand Down Expand Up @@ -199,16 +199,15 @@ async def on_GET(self, request, medium):

# Otherwise show the success template
html = self.config.email_password_reset_template_success_html
request.setResponseCode(200)
status_code = 200
except ThreepidValidationError as e:
request.setResponseCode(e.code)
status_code = e.code

# Show a failure page with a reason
template_vars = {"failure_reason": e.msg}
html = self.failure_email_template.render(**template_vars)

request.write(html.encode("utf-8"))
finish_request(request)
respond_with_html(request, status_code, html)


class PasswordRestServlet(RestServlet):
Expand Down Expand Up @@ -571,16 +570,15 @@ async def on_GET(self, request):

# Otherwise show the success template
html = self.config.email_add_threepid_template_success_html_content
request.setResponseCode(200)
status_code = 200
except ThreepidValidationError as e:
request.setResponseCode(e.code)
status_code = e.code

# Show a failure page with a reason
template_vars = {"failure_reason": e.msg}
html = self.failure_email_template.render(**template_vars)

request.write(html.encode("utf-8"))
finish_request(request)
respond_with_html(request, status_code, html)


class AddThreepidMsisdnSubmitTokenServlet(RestServlet):
Expand Down
11 changes: 2 additions & 9 deletions synapse/rest/client/v2_alpha/account_validity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import logging

from synapse.api.errors import AuthError, SynapseError
from synapse.http.server import finish_request
from synapse.http.server import respond_with_html
from synapse.http.servlet import RestServlet

from ._base import client_patterns
Expand All @@ -26,9 +26,6 @@

class AccountValidityRenewServlet(RestServlet):
PATTERNS = client_patterns("/account_validity/renew$")
SUCCESS_HTML = (
b"<html><body>Your account has been successfully renewed.</body><html>"
)

def __init__(self, hs):
"""
Expand Down Expand Up @@ -59,11 +56,7 @@ async def on_GET(self, request):
status_code = 404
response = self.failure_html

request.setResponseCode(status_code)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(response),))
request.write(response.encode("utf8"))
finish_request(request)
respond_with_html(request, status_code, response)


class AccountValiditySendMailServlet(RestServlet):
Expand Down
18 changes: 3 additions & 15 deletions synapse/rest/client/v2_alpha/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from synapse.api.constants import LoginType
from synapse.api.errors import SynapseError
from synapse.api.urls import CLIENT_API_PREFIX
from synapse.http.server import finish_request
from synapse.http.server import respond_with_html
from synapse.http.servlet import RestServlet, parse_string

from ._base import client_patterns
Expand Down Expand Up @@ -200,13 +200,7 @@ async def on_GET(self, request, stagetype):
raise SynapseError(404, "Unknown auth stage type")

# Render the HTML and return.
html_bytes = html.encode("utf8")
request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

request.write(html_bytes)
finish_request(request)
respond_with_html(request, 200, html)
return None

async def on_POST(self, request, stagetype):
Expand Down Expand Up @@ -263,13 +257,7 @@ async def on_POST(self, request, stagetype):
raise SynapseError(404, "Unknown auth stage type")

# Render the HTML and return.
html_bytes = html.encode("utf8")
request.setResponseCode(200)
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))

request.write(html_bytes)
finish_request(request)
respond_with_html(request, 200, html)
return None

def on_OPTIONS(self, _):
Expand Down
10 changes: 4 additions & 6 deletions synapse/rest/client/v2_alpha/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from synapse.config.registration import RegistrationConfig
from synapse.config.server import is_threepid_reserved
from synapse.handlers.auth import AuthHandler
from synapse.http.server import finish_request
from synapse.http.server import finish_request, respond_with_html
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
Expand Down Expand Up @@ -306,17 +306,15 @@ async def on_GET(self, request, medium):

# Otherwise show the success template
html = self.config.email_registration_template_success_html_content

request.setResponseCode(200)
status_code = 200
except ThreepidValidationError as e:
request.setResponseCode(e.code)
status_code = e.code

# Show a failure page with a reason
template_vars = {"failure_reason": e.msg}
html = self.failure_email_template.render(**template_vars)

request.write(html.encode("utf-8"))
finish_request(request)
respond_with_html(request, status_code, html)


class UsernameAvailabilityRestServlet(RestServlet):
Expand Down
Loading

0 comments on commit ea26e9a

Please sign in to comment.