From: Ralf Jung Date: Sun, 10 Jun 2018 17:30:21 +0000 (+0200) Subject: describe my mailman CAPTCHA X-Git-Url: https://git.ralfj.de/web.git/commitdiff_plain/a2e15867eeeec48d4fe52e1fe89f62f063ecf148?ds=sidebyside describe my mailman CAPTCHA --- diff --git a/personal/_posts/2018-06-02-mailman-subscription-spam.md b/personal/_posts/2018-06-02-mailman-subscription-spam.md index 3c29d1f..e8d557c 100644 --- a/personal/_posts/2018-06-02-mailman-subscription-spam.md +++ b/personal/_posts/2018-06-02-mailman-subscription-spam.md @@ -45,9 +45,10 @@ have found my servers so far are much less patient than that, just setting spam. So, if you are reading this and running a Mailman installation: **Please set -`SUBSCRIBE_FORM_SECRET` and protect your setup against abuse!** Just run `pwgen -16` to get some random string, and then add `SUBSCRIBE_FORM_SECRET = ""` to `/etc/mailman/mm_cfg.py`. It's really that simple! Just a +`SUBSCRIBE_FORM_SECRET` and protect your setup against abuse!** Just run +`openssl rand -base64 18` to get some random string, and then add +`SUBSCRIBE_FORM_SECRET = ""` to `/etc/mailman/mm_cfg.py`. +It's really that simple! Just a [four-line patch in my Ansible playbook](https://git.ralfj.de/ansible.git/commitdiff/937b170594be82e500ae726dc47de8ca9ef3dfcf) to get this rolled out to all servers. Note that you need to be at least on Mailman 2.1.16 for this to work; all currently supported versions of Debian come diff --git a/personal/_posts/2018-06-10-mailman-subscription-spam-continued.md b/personal/_posts/2018-06-10-mailman-subscription-spam-continued.md new file mode 100644 index 0000000..4b92caf --- /dev/null +++ b/personal/_posts/2018-06-10-mailman-subscription-spam-continued.md @@ -0,0 +1,180 @@ +--- +title: "Fighting Mailman Subscription Spam: Leveling Up" +categories: sysadmin +--- + +Last week, I blogged about [my efforts to fight mailman subscription spam]({{ +site.baseurl }}{% post_url 2018-06-02-mailman-subscription-spam %}). Enabling +`SUBSCRIBE_FORM_SECRET` as described there indeed helped to drastically reduce +the amount of subscription spam from more than 1000 to less than 10 mails sent +per day, but some attackers still got through. My guess is that those machines +were just so slow that they managed to wait the required five seconds before +submitting the form. + +So, clearly I had to level up my game. I decided to pull through on my plan to +write a simple CAPTCHA for mailman (that doesn't expose your users to Google). +This post describes how to configure and install that CAPTCHA. + + + +### CAPTCHA configuration + +This simple CAPTCHA is based on a list of questions and matching answers that +you, the site admin, define. The idea is to use questions that anyone who is +interested in your site can easily answer. Since most sites are small enough +that they are not to be targeted specifically, the bots (hopefully) will not be +able to answer these questions. At least for my sites, that has worked so far +(I am running with this patch for a week now). + +The CAPTCHA requires `SUBSCRIBE_FORM_SECRET` to be enabled. Configuration can +look something like this: +``` +SUBSCRIBE_FORM_SECRET = "" +CAPTCHAS = [ + # This is a list of questions, each paired with a list of answers. + ('What is two times six?', ['12', 'twelve']), + ('What is the name of this site's blog', ['Ralf's Ramblings']), +] +``` + +### CAPTCHA patch + +Right now, the `CAPTCHAS` part of the configuration will not yet do anything +because you still have to install my patch. The patch is losely based on +[this blog post](https://warmcat.com/2017/08/12/mailman-captcha.html) and was +written against Mailman 2.1.23 on Debian 9 "Stretch". If you are using a +different version you may have to adapt it accordingly. + +First of all, create a new file `/usr/lib/mailman/Mailman/Captcha.py` with the +following content: +``` +import random +from Mailman import Utils + +def display(mlist, captchas): + """Returns a CAPTCHA question, the HTML for the answer box, and + the data to be put into the CSRF token""" + idx = random.randrange(len(captchas)) + question = captchas[idx][0] + box_html = mlist.FormatBox('captcha_answer', size=30) + return (Utils.websafe(question), box_html, str(idx)) + +def verify(idx, given_answer, captchas): + try: + idx = int(idx) + except ValueError: + return False + if not idx in range(len(captchas)): + return False + # Chec the given answer + correct_answers = captchas[idx][1] + given_answer = given_answer.strip().lower() + return given_answer in map(lambda x: x.strip().lower(), correct_answers) +``` +This contains the actual CAPTCHA logic. Now it needs to be wired up with the +listinfo page (where the subscription form is shown to the user) and the +subscription page (where the subscription form is submitted to). + +Here is the patch for `/usr/lib/mailman/Mailman/Cgi/listinfo.py`: +``` +--- listinfo.py.orig 2018-06-03 19:18:30.089902948 +0200 ++++ listinfo.py 2018-06-10 19:12:59.381910750 +0200 +@@ -26,6 +26,7 @@ + + from Mailman import mm_cfg + from Mailman import Utils ++from Mailman import Captcha + from Mailman import MailList + from Mailman import Errors + from Mailman import i18n +@@ -216,10 +220,16 @@ + # drop one : resulting in an invalid format, but it's only + # for our hash so it doesn't matter. + remote = remote.rsplit(':', 1)[0] ++ # get CAPTCHA data ++ (captcha_question, captcha_box, captcha_idx) = Captcha.display(mlist, mm_cfg.CAPTCHAS) ++ replacements[''] = captcha_question ++ replacements[''] = captcha_box ++ # 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() +``` +And here the patch for `/usr/lib/mailman/Mailman/Cgi/subscribe.py`: +``` +--- subscribe.py.orig 2018-06-03 19:18:35.761813517 +0200 ++++ subscribe.py 2018-06-03 20:35:00.056454989 +0200 +@@ -25,6 +25,7 @@ + + from Mailman import mm_cfg + from Mailman import Utils ++from Mailman import Captcha + from Mailman import MailList + from Mailman import Errors + from Mailman import i18n +@@ -144,13 +147,14 @@ + # for our hash so it doesn't matter. + remote1 = remote.rsplit(':', 1)[0] + try: +- ftime, fhash = cgidata.getvalue('sub_form_token', '').split(':') ++ ftime, fcaptcha_idx, fhash = cgidata.getvalue('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: +@@ -165,6 +169,10 @@ + 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 ++ captcha_answer = cgidata.getvalue('captcha_answer', '') ++ if not 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) +``` + +Finally, the HTML template for the listinfo page needs to be updated to show the CAPTCHA question and answer box. +On Debian, the templates for enabled languages are located in `/etc/mailman/`. +The patch for the English template looks as follows: +``` +--- /usr/share/mailman/en/listinfo.html 2018-02-08 07:54:28.000000000 +0100 ++++ listinfo.html 2018-06-03 20:35:10.680275026 +0200 +@@ -116,6 +116,12 @@ + + + ++ Please answer the following question to prove that you are not a bot: ++ ++ ++ ++ ++ + +
+ +``` + +If you have other languages enabled, you have to translate this patch +accordingly. + +That's it! Now bots have to be adapted to your specific questions to be able to +successfully subscribe someone. It is still a good idea to monitor the logs +(`/var/log/mailman/subscribe` on Debian) to see if any illegitimate requests +still make it through, but unless you site is really big I'd be rather surprised +to see bots being able to answer site-specific questions.