Ensure that HTML pages served from Synapse include headers to avoid embedding.

This commit is contained in:
Patrick Cloke 2020-07-01 09:10:23 -04:00
parent 0fc5575c5b
commit ea26e9a98b
10 changed files with 103 additions and 94 deletions

View file

@ -56,6 +56,7 @@ from synapse.http.server import (
OptionsResource, OptionsResource,
RootOptionsRedirectResource, RootOptionsRedirectResource,
RootRedirect, RootRedirect,
StaticResource,
) )
from synapse.http.site import SynapseSite from synapse.http.site import SynapseSite
from synapse.logging.context import LoggingContext from synapse.logging.context import LoggingContext
@ -228,7 +229,7 @@ class SynapseHomeServer(HomeServer):
if name in ["static", "client"]: if name in ["static", "client"]:
resources.update( resources.update(
{ {
STATIC_PREFIX: File( STATIC_PREFIX: StaticResource(
os.path.join(os.path.dirname(synapse.__file__), "static") os.path.join(os.path.dirname(synapse.__file__), "static")
) )
} }

View file

@ -38,7 +38,7 @@ from synapse.api.errors import (
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker 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.http.site import SynapseRequest
from synapse.logging.context import defer_to_thread from synapse.logging.context import defer_to_thread
from synapse.metrics.background_process_metrics import run_as_background_process from synapse.metrics.background_process_metrics import run_as_background_process
@ -1055,13 +1055,8 @@ class AuthHandler(BaseHandler):
) )
# Render the HTML and return. # Render the HTML and return.
html_bytes = self._sso_auth_success_template.encode("utf-8") html = self._sso_auth_success_template
request.setResponseCode(200) respond_with_html(request, 200, html)
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)
async def complete_sso_login( async def complete_sso_login(
self, self,
@ -1081,13 +1076,7 @@ class AuthHandler(BaseHandler):
# flow. # flow.
deactivated = await self.store.get_user_deactivated_status(registered_user_id) deactivated = await self.store.get_user_deactivated_status(registered_user_id)
if deactivated: if deactivated:
html_bytes = self._sso_account_deactivated_template.encode("utf-8") respond_with_html(request, 403, self._sso_account_deactivated_template)
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)
return return
self._complete_sso_login(registered_user_id, request, client_redirect_url) self._complete_sso_login(registered_user_id, request, client_redirect_url)
@ -1128,17 +1117,12 @@ class AuthHandler(BaseHandler):
# URL we redirect users to. # URL we redirect users to.
redirect_url_no_params = client_redirect_url.split("?")[0] 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, display_url=redirect_url_no_params,
redirect_url=redirect_url, redirect_url=redirect_url,
server_name=self._server_name, server_name=self._server_name,
).encode("utf-8") )
respond_with_html(request, 200, html)
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)
@staticmethod @staticmethod
def add_query_param_to_url(url: str, param_name: str, param: Any): def add_query_param_to_url(url: str, param_name: str, param: Any):

View file

@ -35,7 +35,7 @@ from typing_extensions import TypedDict
from twisted.web.client import readBody from twisted.web.client import readBody
from synapse.config import ConfigError 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.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable from synapse.logging.context import make_deferred_yieldable
from synapse.push.mailer import load_jinja2_templates from synapse.push.mailer import load_jinja2_templates
@ -144,15 +144,10 @@ class OidcHandler:
access_denied. access_denied.
error_description: A human-readable description of the error. 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 error=error, error_description=error_description
).encode("utf-8") )
respond_with_html(request, 400, html)
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)
def _validate_metadata(self): def _validate_metadata(self):
"""Verifies the provider metadata. """Verifies the provider metadata.

View file

@ -30,7 +30,7 @@ from twisted.internet import defer
from twisted.python import failure from twisted.python import failure
from twisted.web import resource from twisted.web import resource
from twisted.web.server import NOT_DONE_YET, Request 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 from twisted.web.util import redirectTo
import synapse.events import synapse.events
@ -202,12 +202,7 @@ def return_html_error(
else: else:
body = error_template.render(code=code, msg=msg) body = error_template.render(code=code, msg=msg)
body_bytes = body.encode("utf-8") respond_with_html(request, code, body)
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)
def wrap_async_request_handler(h): def wrap_async_request_handler(h):
@ -420,6 +415,18 @@ class DirectServeResource(resource.Resource):
return NOT_DONE_YET 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): def _options_handler(request):
"""Request handler for OPTIONS requests """Request handler for OPTIONS requests
@ -530,7 +537,7 @@ def respond_with_json_bytes(
code (int): The HTTP response code. code (int): The HTTP response code.
json_bytes (bytes): The json bytes to use as the response body. json_bytes (bytes): The json bytes to use as the response body.
send_cors (bool): Whether to send Cross-Origin Resource Sharing headers 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: Returns:
twisted.web.server.NOT_DONE_YET""" 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): def finish_request(request):
""" Finish writing the response to the request. """ Finish writing the response to the request.

View file

@ -16,7 +16,7 @@
import logging import logging
from synapse.api.errors import Codes, StoreError, SynapseError 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 ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
@ -177,13 +177,9 @@ class PushersRemoveRestServlet(RestServlet):
self.notifier.on_new_replication_data() self.notifier.on_new_replication_data()
request.setResponseCode(200) respond_with_html_bytes(
request.setHeader(b"Content-Type", b"text/html; charset=utf-8") request, 200, PushersRemoveRestServlet.SUCCESS_HTML,
request.setHeader(
b"Content-Length", b"%d" % (len(PushersRemoveRestServlet.SUCCESS_HTML),)
) )
request.write(PushersRemoveRestServlet.SUCCESS_HTML)
finish_request(request)
return None return None
def on_OPTIONS(self, _): def on_OPTIONS(self, _):

View file

@ -21,7 +21,7 @@ from six.moves import http_client
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
from synapse.config.emailconfig import ThreepidBehaviour 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 ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
@ -199,16 +199,15 @@ class PasswordResetSubmitTokenServlet(RestServlet):
# Otherwise show the success template # Otherwise show the success template
html = self.config.email_password_reset_template_success_html html = self.config.email_password_reset_template_success_html
request.setResponseCode(200) status_code = 200
except ThreepidValidationError as e: except ThreepidValidationError as e:
request.setResponseCode(e.code) status_code = e.code
# Show a failure page with a reason # Show a failure page with a reason
template_vars = {"failure_reason": e.msg} template_vars = {"failure_reason": e.msg}
html = self.failure_email_template.render(**template_vars) html = self.failure_email_template.render(**template_vars)
request.write(html.encode("utf-8")) respond_with_html(request, status_code, html)
finish_request(request)
class PasswordRestServlet(RestServlet): class PasswordRestServlet(RestServlet):
@ -571,16 +570,15 @@ class AddThreepidEmailSubmitTokenServlet(RestServlet):
# Otherwise show the success template # Otherwise show the success template
html = self.config.email_add_threepid_template_success_html_content html = self.config.email_add_threepid_template_success_html_content
request.setResponseCode(200) status_code = 200
except ThreepidValidationError as e: except ThreepidValidationError as e:
request.setResponseCode(e.code) status_code = e.code
# Show a failure page with a reason # Show a failure page with a reason
template_vars = {"failure_reason": e.msg} template_vars = {"failure_reason": e.msg}
html = self.failure_email_template.render(**template_vars) html = self.failure_email_template.render(**template_vars)
request.write(html.encode("utf-8")) respond_with_html(request, status_code, html)
finish_request(request)
class AddThreepidMsisdnSubmitTokenServlet(RestServlet): class AddThreepidMsisdnSubmitTokenServlet(RestServlet):

View file

@ -16,7 +16,7 @@
import logging import logging
from synapse.api.errors import AuthError, SynapseError 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 synapse.http.servlet import RestServlet
from ._base import client_patterns from ._base import client_patterns
@ -26,9 +26,6 @@ logger = logging.getLogger(__name__)
class AccountValidityRenewServlet(RestServlet): class AccountValidityRenewServlet(RestServlet):
PATTERNS = client_patterns("/account_validity/renew$") PATTERNS = client_patterns("/account_validity/renew$")
SUCCESS_HTML = (
b"<html><body>Your account has been successfully renewed.</body><html>"
)
def __init__(self, hs): def __init__(self, hs):
""" """
@ -59,11 +56,7 @@ class AccountValidityRenewServlet(RestServlet):
status_code = 404 status_code = 404
response = self.failure_html response = self.failure_html
request.setResponseCode(status_code) respond_with_html(request, status_code, response)
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)
class AccountValiditySendMailServlet(RestServlet): class AccountValiditySendMailServlet(RestServlet):

View file

@ -18,7 +18,7 @@ import logging
from synapse.api.constants import LoginType from synapse.api.constants import LoginType
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.api.urls import CLIENT_API_PREFIX 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 synapse.http.servlet import RestServlet, parse_string
from ._base import client_patterns from ._base import client_patterns
@ -200,13 +200,7 @@ class AuthRestServlet(RestServlet):
raise SynapseError(404, "Unknown auth stage type") raise SynapseError(404, "Unknown auth stage type")
# Render the HTML and return. # Render the HTML and return.
html_bytes = html.encode("utf8") respond_with_html(request, 200, html)
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)
return None return None
async def on_POST(self, request, stagetype): async def on_POST(self, request, stagetype):
@ -263,13 +257,7 @@ class AuthRestServlet(RestServlet):
raise SynapseError(404, "Unknown auth stage type") raise SynapseError(404, "Unknown auth stage type")
# Render the HTML and return. # Render the HTML and return.
html_bytes = html.encode("utf8") respond_with_html(request, 200, html)
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)
return None return None
def on_OPTIONS(self, _): def on_OPTIONS(self, _):

View file

@ -38,7 +38,7 @@ from synapse.config.ratelimiting import FederationRateLimitConfig
from synapse.config.registration import RegistrationConfig from synapse.config.registration import RegistrationConfig
from synapse.config.server import is_threepid_reserved from synapse.config.server import is_threepid_reserved
from synapse.handlers.auth import AuthHandler 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 ( from synapse.http.servlet import (
RestServlet, RestServlet,
assert_params_in_dict, assert_params_in_dict,
@ -306,17 +306,15 @@ class RegistrationSubmitTokenServlet(RestServlet):
# Otherwise show the success template # Otherwise show the success template
html = self.config.email_registration_template_success_html_content html = self.config.email_registration_template_success_html_content
status_code = 200
request.setResponseCode(200)
except ThreepidValidationError as e: except ThreepidValidationError as e:
request.setResponseCode(e.code) status_code = e.code
# Show a failure page with a reason # Show a failure page with a reason
template_vars = {"failure_reason": e.msg} template_vars = {"failure_reason": e.msg}
html = self.failure_email_template.render(**template_vars) html = self.failure_email_template.render(**template_vars)
request.write(html.encode("utf-8")) respond_with_html(request, status_code, html)
finish_request(request)
class UsernameAvailabilityRestServlet(RestServlet): class UsernameAvailabilityRestServlet(RestServlet):

View file

@ -29,7 +29,7 @@ from synapse.api.errors import NotFoundError, StoreError, SynapseError
from synapse.config import ConfigError from synapse.config import ConfigError
from synapse.http.server import ( from synapse.http.server import (
DirectServeResource, DirectServeResource,
finish_request, respond_with_html,
wrap_html_request_handler, wrap_html_request_handler,
) )
from synapse.http.servlet import parse_string from synapse.http.servlet import parse_string
@ -197,12 +197,8 @@ class ConsentResource(DirectServeResource):
template_html = self._jinja_env.get_template( template_html = self._jinja_env.get_template(
path.join(TEMPLATE_LANGUAGE, template_name) path.join(TEMPLATE_LANGUAGE, template_name)
) )
html_bytes = template_html.render(**template_args).encode("utf8") html = template_html.render(**template_args)
respond_with_html(request, 200, html)
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)
def _check_hash(self, userid, userhmac): def _check_hash(self, userid, userhmac):
""" """