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

Commit ea26e9a

Browse files
committed
Ensure that HTML pages served from Synapse include headers to avoid embedding.
1 parent 0fc5575 commit ea26e9a

File tree

10 files changed

+103
-94
lines changed

10 files changed

+103
-94
lines changed

synapse/app/homeserver.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
OptionsResource,
5757
RootOptionsRedirectResource,
5858
RootRedirect,
59+
StaticResource,
5960
)
6061
from synapse.http.site import SynapseSite
6162
from synapse.logging.context import LoggingContext
@@ -228,7 +229,7 @@ def _configure_named_resource(self, name, compress=False):
228229
if name in ["static", "client"]:
229230
resources.update(
230231
{
231-
STATIC_PREFIX: File(
232+
STATIC_PREFIX: StaticResource(
232233
os.path.join(os.path.dirname(synapse.__file__), "static")
233234
)
234235
}

synapse/handlers/auth.py

+7-23
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from synapse.api.ratelimiting import Ratelimiter
3939
from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
4040
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
41-
from synapse.http.server import finish_request
41+
from synapse.http.server import finish_request, respond_with_html
4242
from synapse.http.site import SynapseRequest
4343
from synapse.logging.context import defer_to_thread
4444
from synapse.metrics.background_process_metrics import run_as_background_process
@@ -1055,13 +1055,8 @@ async def complete_sso_ui_auth(
10551055
)
10561056

10571057
# Render the HTML and return.
1058-
html_bytes = self._sso_auth_success_template.encode("utf-8")
1059-
request.setResponseCode(200)
1060-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
1061-
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
1062-
1063-
request.write(html_bytes)
1064-
finish_request(request)
1058+
html = self._sso_auth_success_template
1059+
respond_with_html(request, 200, html)
10651060

10661061
async def complete_sso_login(
10671062
self,
@@ -1081,13 +1076,7 @@ async def complete_sso_login(
10811076
# flow.
10821077
deactivated = await self.store.get_user_deactivated_status(registered_user_id)
10831078
if deactivated:
1084-
html_bytes = self._sso_account_deactivated_template.encode("utf-8")
1085-
1086-
request.setResponseCode(403)
1087-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
1088-
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
1089-
request.write(html_bytes)
1090-
finish_request(request)
1079+
respond_with_html(request, 403, self._sso_account_deactivated_template)
10911080
return
10921081

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

1131-
html_bytes = self._sso_redirect_confirm_template.render(
1120+
html = self._sso_redirect_confirm_template.render(
11321121
display_url=redirect_url_no_params,
11331122
redirect_url=redirect_url,
11341123
server_name=self._server_name,
1135-
).encode("utf-8")
1136-
1137-
request.setResponseCode(200)
1138-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
1139-
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
1140-
request.write(html_bytes)
1141-
finish_request(request)
1124+
)
1125+
respond_with_html(request, 200, html)
11421126

11431127
@staticmethod
11441128
def add_query_param_to_url(url: str, param_name: str, param: Any):

synapse/handlers/oidc_handler.py

+4-9
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from twisted.web.client import readBody
3636

3737
from synapse.config import ConfigError
38-
from synapse.http.server import finish_request
38+
from synapse.http.server import respond_with_html
3939
from synapse.http.site import SynapseRequest
4040
from synapse.logging.context import make_deferred_yieldable
4141
from synapse.push.mailer import load_jinja2_templates
@@ -144,15 +144,10 @@ def _render_error(
144144
access_denied.
145145
error_description: A human-readable description of the error.
146146
"""
147-
html_bytes = self._error_template.render(
147+
html = self._error_template.render(
148148
error=error, error_description=error_description
149-
).encode("utf-8")
150-
151-
request.setResponseCode(400)
152-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
153-
request.setHeader(b"Content-Length", b"%i" % len(html_bytes))
154-
request.write(html_bytes)
155-
finish_request(request)
149+
)
150+
respond_with_html(request, 400, html)
156151

157152
def _validate_metadata(self):
158153
"""Verifies the provider metadata.

synapse/http/server.py

+68-8
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from twisted.python import failure
3131
from twisted.web import resource
3232
from twisted.web.server import NOT_DONE_YET, Request
33-
from twisted.web.static import NoRangeStaticProducer
33+
from twisted.web.static import File, NoRangeStaticProducer
3434
from twisted.web.util import redirectTo
3535

3636
import synapse.events
@@ -202,12 +202,7 @@ def return_html_error(
202202
else:
203203
body = error_template.render(code=code, msg=msg)
204204

205-
body_bytes = body.encode("utf-8")
206-
request.setResponseCode(code)
207-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
208-
request.setHeader(b"Content-Length", b"%i" % (len(body_bytes),))
209-
request.write(body_bytes)
210-
finish_request(request)
205+
respond_with_html(request, code, body)
211206

212207

213208
def wrap_async_request_handler(h):
@@ -420,6 +415,18 @@ def render(self, request):
420415
return NOT_DONE_YET
421416

422417

418+
class StaticResource(File):
419+
"""
420+
A resource that represents a plain non-interpreted file or directory.
421+
422+
Differs from the File resource by adding clickjacking protection.
423+
"""
424+
425+
def render_GET(self, request: Request):
426+
set_clickjacking_protection_headers(request)
427+
return super().render_GET(request)
428+
429+
423430
def _options_handler(request):
424431
"""Request handler for OPTIONS requests
425432
@@ -530,7 +537,7 @@ def respond_with_json_bytes(
530537
code (int): The HTTP response code.
531538
json_bytes (bytes): The json bytes to use as the response body.
532539
send_cors (bool): Whether to send Cross-Origin Resource Sharing headers
533-
http://www.w3.org/TR/cors/
540+
https://fetch.spec.whatwg.org/#http-cors-protocol
534541
Returns:
535542
twisted.web.server.NOT_DONE_YET"""
536543

@@ -568,6 +575,59 @@ def set_cors_headers(request):
568575
)
569576

570577

578+
def respond_with_html(request: Request, code: int, html: str):
579+
"""
580+
Wraps `respond_with_html_bytes` by first encoding HTML from a str to UTF-8 bytes.
581+
"""
582+
respond_with_html_bytes(request, code, html.encode("utf-8"))
583+
584+
585+
def respond_with_html_bytes(request: Request, code: int, html_bytes: bytes):
586+
"""
587+
Sends HTML (encoded as UTF-8 bytes) as the response to the given request.
588+
589+
Note that this adds clickjacking protection headers and finishes the request.
590+
591+
Args:
592+
request: The http request to respond to.
593+
code: The HTTP response code.
594+
html_bytes: The HTML bytes to use as the response body.
595+
"""
596+
# could alternatively use request.notifyFinish() and flip a flag when
597+
# the Deferred fires, but since the flag is RIGHT THERE it seems like
598+
# a waste.
599+
if request._disconnected:
600+
logger.warning(
601+
"Not sending response to request %s, already disconnected.", request
602+
)
603+
return
604+
605+
request.setResponseCode(code)
606+
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
607+
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
608+
609+
# Ensure this content cannot be embedded.
610+
set_clickjacking_protection_headers(request)
611+
612+
request.write(html_bytes)
613+
finish_request(request)
614+
615+
616+
def set_clickjacking_protection_headers(request: Request):
617+
"""
618+
Set headers to guard against clickjacking of embedded content.
619+
620+
This sets the X-Frame-Options and Content-Security-Policy headers which instructs
621+
browsers to not allow the HTML of the response to be embedded onto another
622+
page.
623+
624+
Args:
625+
request: The http request to add the headers to.
626+
"""
627+
request.setHeader(b"X-Frame-Options", b"DENY")
628+
request.setHeader(b"Content-Security-Policy", b"frame-ancestors 'none';")
629+
630+
571631
def finish_request(request):
572632
""" Finish writing the response to the request.
573633

synapse/rest/client/v1/pusher.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717

1818
from synapse.api.errors import Codes, StoreError, SynapseError
19-
from synapse.http.server import finish_request
19+
from synapse.http.server import respond_with_html_bytes
2020
from synapse.http.servlet import (
2121
RestServlet,
2222
assert_params_in_dict,
@@ -177,13 +177,9 @@ async def on_GET(self, request):
177177

178178
self.notifier.on_new_replication_data()
179179

180-
request.setResponseCode(200)
181-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
182-
request.setHeader(
183-
b"Content-Length", b"%d" % (len(PushersRemoveRestServlet.SUCCESS_HTML),)
180+
respond_with_html_bytes(
181+
request, 200, PushersRemoveRestServlet.SUCCESS_HTML,
184182
)
185-
request.write(PushersRemoveRestServlet.SUCCESS_HTML)
186-
finish_request(request)
187183
return None
188184

189185
def on_OPTIONS(self, _):

synapse/rest/client/v2_alpha/account.py

+7-9
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from synapse.api.constants import LoginType
2222
from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
2323
from synapse.config.emailconfig import ThreepidBehaviour
24-
from synapse.http.server import finish_request
24+
from synapse.http.server import finish_request, respond_with_html
2525
from synapse.http.servlet import (
2626
RestServlet,
2727
assert_params_in_dict,
@@ -199,16 +199,15 @@ async def on_GET(self, request, medium):
199199

200200
# Otherwise show the success template
201201
html = self.config.email_password_reset_template_success_html
202-
request.setResponseCode(200)
202+
status_code = 200
203203
except ThreepidValidationError as e:
204-
request.setResponseCode(e.code)
204+
status_code = e.code
205205

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

210-
request.write(html.encode("utf-8"))
211-
finish_request(request)
210+
respond_with_html(request, status_code, html)
212211

213212

214213
class PasswordRestServlet(RestServlet):
@@ -571,16 +570,15 @@ async def on_GET(self, request):
571570

572571
# Otherwise show the success template
573572
html = self.config.email_add_threepid_template_success_html_content
574-
request.setResponseCode(200)
573+
status_code = 200
575574
except ThreepidValidationError as e:
576-
request.setResponseCode(e.code)
575+
status_code = e.code
577576

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

582-
request.write(html.encode("utf-8"))
583-
finish_request(request)
581+
respond_with_html(request, status_code, html)
584582

585583

586584
class AddThreepidMsisdnSubmitTokenServlet(RestServlet):

synapse/rest/client/v2_alpha/account_validity.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717

1818
from synapse.api.errors import AuthError, SynapseError
19-
from synapse.http.server import finish_request
19+
from synapse.http.server import respond_with_html
2020
from synapse.http.servlet import RestServlet
2121

2222
from ._base import client_patterns
@@ -26,9 +26,6 @@
2626

2727
class AccountValidityRenewServlet(RestServlet):
2828
PATTERNS = client_patterns("/account_validity/renew$")
29-
SUCCESS_HTML = (
30-
b"<html><body>Your account has been successfully renewed.</body><html>"
31-
)
3229

3330
def __init__(self, hs):
3431
"""
@@ -59,11 +56,7 @@ async def on_GET(self, request):
5956
status_code = 404
6057
response = self.failure_html
6158

62-
request.setResponseCode(status_code)
63-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
64-
request.setHeader(b"Content-Length", b"%d" % (len(response),))
65-
request.write(response.encode("utf8"))
66-
finish_request(request)
59+
respond_with_html(request, status_code, response)
6760

6861

6962
class AccountValiditySendMailServlet(RestServlet):

synapse/rest/client/v2_alpha/auth.py

+3-15
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from synapse.api.constants import LoginType
1919
from synapse.api.errors import SynapseError
2020
from synapse.api.urls import CLIENT_API_PREFIX
21-
from synapse.http.server import finish_request
21+
from synapse.http.server import respond_with_html
2222
from synapse.http.servlet import RestServlet, parse_string
2323

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

202202
# Render the HTML and return.
203-
html_bytes = html.encode("utf8")
204-
request.setResponseCode(200)
205-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
206-
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
207-
208-
request.write(html_bytes)
209-
finish_request(request)
203+
respond_with_html(request, 200, html)
210204
return None
211205

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

265259
# Render the HTML and return.
266-
html_bytes = html.encode("utf8")
267-
request.setResponseCode(200)
268-
request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
269-
request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),))
270-
271-
request.write(html_bytes)
272-
finish_request(request)
260+
respond_with_html(request, 200, html)
273261
return None
274262

275263
def on_OPTIONS(self, _):

synapse/rest/client/v2_alpha/register.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
from synapse.config.registration import RegistrationConfig
3939
from synapse.config.server import is_threepid_reserved
4040
from synapse.handlers.auth import AuthHandler
41-
from synapse.http.server import finish_request
41+
from synapse.http.server import finish_request, respond_with_html
4242
from synapse.http.servlet import (
4343
RestServlet,
4444
assert_params_in_dict,
@@ -306,17 +306,15 @@ async def on_GET(self, request, medium):
306306

307307
# Otherwise show the success template
308308
html = self.config.email_registration_template_success_html_content
309-
310-
request.setResponseCode(200)
309+
status_code = 200
311310
except ThreepidValidationError as e:
312-
request.setResponseCode(e.code)
311+
status_code = e.code
313312

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

318-
request.write(html.encode("utf-8"))
319-
finish_request(request)
317+
respond_with_html(request, status_code, html)
320318

321319

322320
class UsernameAvailabilityRestServlet(RestServlet):

0 commit comments

Comments
 (0)