aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman
diff options
context:
space:
mode:
authorRalf Jung <post@ralfj.de>2019-06-10 17:29:24 +0200
committerRalf Jung <post@ralfj.de>2019-06-10 17:29:24 +0200
commitb7476d1c86053181cb38aa3acd3fc718fde55979 (patch)
tree9aa2c07ef0d77f857d5cbcfeacd19abeaa064840 /Mailman
parent56188e427f80ed350b6608ce47124402c90b9d40 (diff)
downloadmailman2-b7476d1c86053181cb38aa3acd3fc718fde55979.tar.gz
mailman2-b7476d1c86053181cb38aa3acd3fc718fde55979.tar.xz
mailman2-b7476d1c86053181cb38aa3acd3fc718fde55979.zip
implement a simple CAPTCHA scheme based on questions and answers configured by the site admin
Diffstat (limited to 'Mailman')
-rw-r--r--Mailman/Cgi/listinfo.py19
-rw-r--r--Mailman/Cgi/subscribe.py10
-rwxr-xr-xMailman/Defaults.py.in16
-rw-r--r--Mailman/Utils.py29
4 files changed, 70 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['<mm-captcha-ui>'] = (
+ """<tr><td BGCOLOR="#dddddd">%s<br>%s</td><td>%s</td></tr>"""
+ % (pre_question, captcha_question, captcha_box))
+ else:
+ captcha_idx = 0 # just to have something to include in the hash below
+ # fill form
replacements['<mm-subscribe-form-start>'] += (
- '<input type="hidden" name="sub_form_token" value="%s:%s">\n'
- % (now, Utils.sha_new(mm_cfg.SUBSCRIBE_FORM_SECRET + ":" +
+ '<input type="hidden" name="sub_form_token" value="%s:%s:%s">\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)