From 3a8a94448af334e57c5cfa3583b2c20739aeb613 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 23 Sep 2014 14:29:08 +0100 Subject: [PATCH 1/4] Allow a (hidden undocumented) key to m.login.recaptcha to specify a shared secret to allow bots to bypass the ReCAPTCHA test (SYN-60) --- synapse/config/captcha.py | 7 ++++++- synapse/rest/register.py | 23 ++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index 8ebcfc3623..4ed9070b9e 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -24,6 +24,7 @@ class CaptchaConfig(Config): self.captcha_ip_origin_is_x_forwarded = ( args.captcha_ip_origin_is_x_forwarded ) + self.captcha_bypass_secret = args.captcha_bypass_secret @classmethod def add_arguments(cls, parser): @@ -43,4 +44,8 @@ class CaptchaConfig(Config): "--captcha_ip_origin_is_x_forwarded", type=bool, default=False, help="When checking captchas, use the X-Forwarded-For (XFF) header" + " as the client IP and not the actual client IP." - ) \ No newline at end of file + ) + group.add_argument( + "--captcha_bypass_secret", type=str, + help="A secret key used to bypass the captcha test entirely." + ) diff --git a/synapse/rest/register.py b/synapse/rest/register.py index af528a44f6..f1354e4b71 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -142,6 +142,24 @@ class RegisterRestServlet(RestServlet): if not self.hs.config.enable_registration_captcha: raise SynapseError(400, "Captcha not required.") + yield self._check_recaptcha(request, register_json) + + session[LoginType.RECAPTCHA] = True # mark captcha as done + self._save_session(session) + defer.returnValue({ + "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] + }) + + @defer.inlineCallbacks + def _check_recaptcha(self, request, register_json): + if "captcha_bypass_secret" in register_json: + if (register_json["captcha_bypass_secret"] == + self.hs.config.captcha_bypass_secret): + defer.returnValue(None) + else: + raise SynapseError(400, "Captcha bypass secret incorrect", + errcode=Codes.CAPTCHA_NEEDED) + challenge = None user_response = None try: @@ -166,11 +184,6 @@ class RegisterRestServlet(RestServlet): challenge, user_response ) - session[LoginType.RECAPTCHA] = True # mark captcha as done - self._save_session(session) - defer.returnValue({ - "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY] - }) @defer.inlineCallbacks def _do_email_identity(self, request, register_json, session): From 5f16439752fa6ff9b452cac86fbbb07a12ae44f7 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 23 Sep 2014 15:16:47 +0100 Subject: [PATCH 2/4] Make sure the config actually /has/ a captcha_bypass_secret set before trying to compare it --- synapse/rest/register.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/synapse/rest/register.py b/synapse/rest/register.py index f1354e4b71..3b07a127a6 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -153,8 +153,9 @@ class RegisterRestServlet(RestServlet): @defer.inlineCallbacks def _check_recaptcha(self, request, register_json): if "captcha_bypass_secret" in register_json: - if (register_json["captcha_bypass_secret"] == - self.hs.config.captcha_bypass_secret): + if (self.hs.config.captcha_bypass_secret is not None and + register_json["captcha_bypass_secret"] == + self.hs.config.captcha_bypass_secret): defer.returnValue(None) else: raise SynapseError(400, "Captcha bypass secret incorrect", From 537c7e1137684c97676bbb74d59cd93c5bf1aad1 Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 23 Sep 2014 15:18:59 +0100 Subject: [PATCH 3/4] Config values are almost never 'None', but they might be empty string. Detect their presence by truth --- synapse/rest/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 3b07a127a6..66cef26ada 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -153,7 +153,7 @@ class RegisterRestServlet(RestServlet): @defer.inlineCallbacks def _check_recaptcha(self, request, register_json): if "captcha_bypass_secret" in register_json: - if (self.hs.config.captcha_bypass_secret is not None and + if (self.hs.config.captcha_bypass_secret and register_json["captcha_bypass_secret"] == self.hs.config.captcha_bypass_secret): defer.returnValue(None) From c03176af59adfe0d60ffd8beb8bf262b4563d20b Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 23 Sep 2014 15:58:44 +0100 Subject: [PATCH 4/4] Send an HMAC(SHA1) protecting the User ID for the ReCAPTCHA bypass, rather than simply the secret itself, so it's useless if that HMAC leaks --- synapse/rest/register.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 66cef26ada..14d1ab018e 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -21,6 +21,8 @@ from synapse.api.constants import LoginType from base import RestServlet, client_path_pattern import synapse.util.stringutils as stringutils +from hashlib import sha1 +import hmac import json import logging import urllib @@ -142,7 +144,7 @@ class RegisterRestServlet(RestServlet): if not self.hs.config.enable_registration_captcha: raise SynapseError(400, "Captcha not required.") - yield self._check_recaptcha(request, register_json) + yield self._check_recaptcha(request, register_json, session) session[LoginType.RECAPTCHA] = True # mark captcha as done self._save_session(session) @@ -151,14 +153,27 @@ class RegisterRestServlet(RestServlet): }) @defer.inlineCallbacks - def _check_recaptcha(self, request, register_json): - if "captcha_bypass_secret" in register_json: - if (self.hs.config.captcha_bypass_secret and - register_json["captcha_bypass_secret"] == - self.hs.config.captcha_bypass_secret): + def _check_recaptcha(self, request, register_json, session): + if ("captcha_bypass_hmac" in register_json and + self.hs.config.captcha_bypass_secret): + if "user" not in register_json: + raise SynapseError(400, "Captcha bypass needs 'user'") + + want = hmac.new( + key=self.hs.config.captcha_bypass_secret, + msg=register_json["user"], + digestmod=sha1, + ).hexdigest() + + # str() because otherwise hmac complains that 'unicode' does not + # have the buffer interface + got = str(register_json["captcha_bypass_hmac"]) + + if hmac.compare_digest(want, got): + session["user"] = register_json["user"] defer.returnValue(None) else: - raise SynapseError(400, "Captcha bypass secret incorrect", + raise SynapseError(400, "Captcha bypass HMAC incorrect", errcode=Codes.CAPTCHA_NEEDED) challenge = None @@ -209,6 +224,10 @@ class RegisterRestServlet(RestServlet): # captcha should've been done by this stage! raise SynapseError(400, "Captcha is required.") + if ("user" in session and "user" in register_json and + session["user"] != register_json["user"]): + raise SynapseError(400, "Cannot change user ID during registration") + password = register_json["password"].encode("utf-8") desired_user_id = (register_json["user"].encode("utf-8") if "user" in register_json else None)