From b7476d1c86053181cb38aa3acd3fc718fde55979 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 10 Jun 2019 17:29:24 +0200 Subject: implement a simple CAPTCHA scheme based on questions and answers configured by the site admin --- Mailman/Cgi/listinfo.py | 19 +++++++++++++++++-- Mailman/Cgi/subscribe.py | 10 ++++++++-- Mailman/Defaults.py.in | 16 ++++++++++++++++ Mailman/Utils.py | 29 +++++++++++++++++++++++++++++ templates/de/listinfo.html | 1 + templates/en/listinfo.html | 1 + templates/fr/listinfo.html | 1 + 7 files changed, 73 insertions(+), 4 deletions(-) diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py index f1b455da..909e401e 100644 --- a/Mailman/Cgi/listinfo.py +++ b/Mailman/Cgi/listinfo.py @@ -216,10 +216,25 @@ def list_listinfo(mlist, lang): # drop one : resulting in an invalid format, but it's only # for our hash so it doesn't matter. remote = remote.rsplit(':', 1)[0] + # render CAPTCHA, if configured + if isinstance(mm_cfg.CAPTCHAS, dict): + (captcha_question, captcha_box, captcha_idx) = \ + Utils.captcha_display(mlist, lang, mm_cfg.CAPTCHAS) + pre_question = _( + '''Please answer the following question to prove that + you are not a bot:''' + ) + replacements[''] = ( + """%s
%s%s""" + % (pre_question, captcha_question, captcha_box)) + else: + captcha_idx = 0 # just to have something to include in the hash below + # fill form replacements[''] += ( - '\n' - % (now, Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + ":" + + '\n' + % (now, captcha_idx, Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + ":" + now + ":" + + captcha_idx + ":" + mlist.internal_name() + ":" + remote ).hexdigest() diff --git a/Mailman/Cgi/subscribe.py b/Mailman/Cgi/subscribe.py index b6527a2a..7e7ebc61 100644 --- a/Mailman/Cgi/subscribe.py +++ b/Mailman/Cgi/subscribe.py @@ -168,13 +168,14 @@ def process_form(mlist, doc, cgidata, lang): # for our hash so it doesn't matter. remote1 = remote.rsplit(':', 1)[0] try: - ftime, fhash = cgidata.getfirst('sub_form_token', '').split(':') + ftime, fcaptcha_idx, fhash = cgidata.getfirst('sub_form_token', '').split(':') then = int(ftime) except ValueError: - ftime = fhash = '' + ftime = fcaptcha_idx = fhash = '' then = 0 token = Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + ":" + ftime + ":" + + fcaptcha_idx + ":" + mlist.internal_name() + ":" + remote1).hexdigest() if ftime and now - then > mm_cfg.FORM_LIFETIME: @@ -189,6 +190,11 @@ def process_form(mlist, doc, cgidata, lang): results.append( _('There was no hidden token in your submission or it was corrupted.')) results.append(_('You must GET the form before submitting it.')) + # Check captcha + if isinstance(mm_cfg.CAPTCHAS, dict): + captcha_answer = cgidata.getvalue('captcha_answer', '') + if not Utils.captcha_verify(fcaptcha_idx, captcha_answer, mm_cfg.CAPTCHAS): + results.append(_('This was not the right answer to the CAPTCHA question.')) # Was an attempt made to subscribe the list to itself? if email == mlist.GetListEmail(): syslog('mischief', 'Attempt to self subscribe %s: %s', email, remote) diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index 3350f278..6f645953 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -131,6 +131,22 @@ SUBSCRIBE_FORM_SECRET = None # test. SUBSCRIBE_FORM_MIN_TIME = seconds(5) +# Use a custom question-answer CAPTCHA to protect against subscription spam. +# Has no effect unless SUBSCRIBE_FORM_SECRET is set. +# Should be set to a dict mapping language keys to a list of pairs +# of questions and regexes for the answers, e.g. +# CAPTCHAS = { +# 'en': [ +# ('What is two times six?', '(12|twelve)'), +# ], +# 'de': [ +# ('Was ist 3 mal 6?', '(18|achtzehn)'), +# ], +# } +# The regular expression must match the full string, i.e., it is implicitly +# acting as if it had "^" in the beginning and "$" at the end. +CAPTCHAS = None + # Use Google reCAPTCHA to protect the subscription form from spam bots. The # following must be set to a pair of keys issued by the reCAPTCHA service at # https://www.google.com/recaptcha/admin diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 10629fc4..9ab35a1c 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -1576,3 +1576,32 @@ def banned_domain(email): if not re.search(r'127\.0\.1\.255$', text, re.MULTILINE): return True return False + + +def captcha_display(mlist, lang, captchas): + """Returns a CAPTCHA question, the HTML for the answer box, and + the data to be put into the CSRF token""" + if not lang in captchas: + lang = 'en' + captchas = captchas[lang] + idx = random.randrange(len(captchas)) + question = captchas[idx][0] + box_html = mlist.FormatBox('captcha_answer', size=30) + # Remember to encode the language in the index so that we can get it out again! + return (websafe(question), box_html, lang + "-" + str(idx)) + +def captcha_verify(idx, given_answer, captchas): + try: + (lang, idx) = idx.split("-") + idx = int(idx) + except ValueError: + return False + if not lang in captchas: + return False + captchas = captchas[lang] + if not idx in range(len(captchas)): + return False + # Check the given answer. + # We append a `$` to emulate `re.fullmatch`. + correct_answer_pattern = captchas[idx][1] + "$" + return re.match(correct_answer_pattern, given_answer) diff --git a/templates/de/listinfo.html b/templates/de/listinfo.html index 647a66cc..78531d21 100755 --- a/templates/de/listinfo.html +++ b/templates/de/listinfo.html @@ -115,6 +115,7 @@ Liste . +
diff --git a/templates/en/listinfo.html b/templates/en/listinfo.html index c3c216b1..7a27fbdd 100644 --- a/templates/en/listinfo.html +++ b/templates/en/listinfo.html @@ -116,6 +116,7 @@ +
diff --git a/templates/fr/listinfo.html b/templates/fr/listinfo.html index 61954769..6ca7cf55 100644 --- a/templates/fr/listinfo.html +++ b/templates/fr/listinfo.html @@ -119,6 +119,7 @@ +
-- cgit v1.2.3 From 4348ac442749ad4b68dca81c223d8ba8070e654d Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 10 Jun 2019 22:01:51 +0200 Subject: fix computing the form hash when there is no CAPTCHA --- Mailman/Cgi/listinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py index 909e401e..6872613e 100644 --- a/Mailman/Cgi/listinfo.py +++ b/Mailman/Cgi/listinfo.py @@ -228,7 +228,7 @@ def list_listinfo(mlist, lang): """%s
%s%s""" % (pre_question, captcha_question, captcha_box)) else: - captcha_idx = 0 # just to have something to include in the hash below + captcha_idx = "" # just to have something to include in the hash below # fill form replacements[''] += ( '\n' -- cgit v1.2.3 From 496e59f4cc7b4db11a26bfc6ad70bc395f1ffce6 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 10 Jun 2019 22:05:32 +0200 Subject: Mention in the docs that 'en' is used as the default key --- Mailman/Defaults.py.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in index 6f645953..401dadc3 100755 --- a/Mailman/Defaults.py.in +++ b/Mailman/Defaults.py.in @@ -138,6 +138,7 @@ SUBSCRIBE_FORM_MIN_TIME = seconds(5) # CAPTCHAS = { # 'en': [ # ('What is two times six?', '(12|twelve)'), +# ('What is this mailing list software called?', '[Mm]ailman'), # ], # 'de': [ # ('Was ist 3 mal 6?', '(18|achtzehn)'), @@ -145,6 +146,8 @@ SUBSCRIBE_FORM_MIN_TIME = seconds(5) # } # The regular expression must match the full string, i.e., it is implicitly # acting as if it had "^" in the beginning and "$" at the end. +# An 'en' key must be present and is used as fall-back if there are no questions +# for the currently set language. CAPTCHAS = None # Use Google reCAPTCHA to protect the subscription form from spam bots. The -- cgit v1.2.3 From 91203be694e4ca836b862b7921e119b2f55a8307 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 10 Jun 2019 22:06:47 +0200 Subject: Don't enable CAPTCHA if 'en' key is not set --- Mailman/Cgi/listinfo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mailman/Cgi/listinfo.py b/Mailman/Cgi/listinfo.py index 6872613e..b35b8988 100644 --- a/Mailman/Cgi/listinfo.py +++ b/Mailman/Cgi/listinfo.py @@ -217,7 +217,7 @@ def list_listinfo(mlist, lang): # for our hash so it doesn't matter. remote = remote.rsplit(':', 1)[0] # render CAPTCHA, if configured - if isinstance(mm_cfg.CAPTCHAS, dict): + if isinstance(mm_cfg.CAPTCHAS, dict) and 'en' in mm_cfg.CAPTCHAS: (captcha_question, captcha_box, captcha_idx) = \ Utils.captcha_display(mlist, lang, mm_cfg.CAPTCHAS) pre_question = _( -- cgit v1.2.3