Coding

Protect your Plone 2.5 sites from spambots with Recaptcha using a PHP script

I help maintain a couple of Plone sites (rechtswirklichkeit.de, rechtssoziologie.info, lsa-berlin.org and others) and there has been an increasing number of automated sign-up attempts by spam bots recently. This does not only bloat the members section of the website with bogus users when these registration attemps are successful, it also spams my inbox with rejected emails from the Plone system when the spam bots use non-existing e-mail addresses.

There are a couple of “Captcha” plugins available from the Plone community (which I found too late), but here is yet another one that will work without installation of a new product, as long as you have access to the ZMI, and uses the excellent Recaptcha service. Here is how you can use their service to protect your Plone site.

1. Sign up with Recaptcha and get keys

Go to the recaptcha home page and sign up. You can then generate a pair of keys that you need in order to use their service.

2. Create PHP script

Download the Recaptcha PHP library and put then into a folder on your server. Create a file index.php and copy & paste this script into the file :

<html>
<body>
<form action="" method="post">
<table>
<tr><td>
<?php

require_once('recaptchalib.php');
// Public and Private Key depending on domain

$domain = $_SERVER['HTTP_HOST'];
$publickey = "THE PUBLIC KEY FROM RECAPTCHA";
$privatekey = "THE PRIVATE KEY FROM RECAPTCHA";
# the response from reCAPTCHA
$resp = null;
# the error code from reCAPTCHA, if any
$error = null;

# was there a reCAPTCHA response?
if ($_POST["recaptcha_response_field"]) {
$resp = recaptcha_check_answer ($privatekey,
$_SERVER["REMOTE_ADDR"],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"]);

if ($resp->is_valid) {
// this allows cross-domain iframe communication
// this is very easy to hack, but of course spammers need to know about it
echo "<script>parent.location = ( 'http://' + location.hash.substr(1) + '#$publickey')</script>";
} else {
# set the error code so that we can display it
$error = $resp->error;
}
}
echo recaptcha_get_html($publickey, $error);
?>
</td><td>
<p style="font-family:Arial;font-size:10px;color:#303030">This is a security measure to prevent spammers from automatically registering with this site.
Please type the words displayed to the left and press the "Submit" button. If you cannot read them, do not enter anything and simply press the button. You will get a new set of images.</p>

</td></tr>
</table>
<input type="submit" value="Submit" />
</form>
</body>
</html>

Make sure to edit the $publickey and $privatekey variable definitions.

3. Customize /portal_skins/custom/join_form

Then go to your plone site and customize the /portal_skins/custom/join_form template like so:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="plone">

<head>

<!-- start edit: modify the metal:block tag and insert the script -->
<metal:block fill-slot="top_slot"
tal:define="dummy python:request.set('disable_border',1)" >
<script type="text/javascript">
// because of the same-domain policy of an iframe, the iframe cannot
// submit this form. We need to listen for an Iframe message
// which consists in changing this documents fragment identifier

var lastId = "";
function checkForMessages(){
if(location.hash != lastId){
lastId = location.hash;
if (lastId == "#YOUR PUBLIC KEY HERE") {
location.hash = "";
document.getElementById("passkey").value=lastId;
document.getElementById("submitButton").style.display="inline";
document.getElementById("submitButton").click();
}
}
}
// we need a timeout otherwise it doesn't work
setTimeout(function(){setInterval(checkForMessages, 200)},5000);
</script>

</metal:block>
<!-- end of edit -->

</head>

<body>
<div metal:fill-slot="main"
tal:define="errors options/state/getErrors;">

<h1 i18n:translate="heading_registration_form">Registration Form</h1>

<!-- start edit: remove 'class="enableUnloadProtection"' from the form tag -->
<form action=""
method="post"
tal:define="allowEnterPassword site_properties/validate_email|nothing;
tal:attributes="action template_id" >

<!-- end edit -->

[ ... core part of the page remains unchanged. scroll down to the bottom ... ]

<div class="formHelp" i18n:translate="label_password_will_be_mailed">
A password will be generated and
e-mailed to you to complete the registration process.
</div>
</div>

<!-- start edit: add this iframe -->
<iframe
id="captchaIframe"
name="captchaIframe"
src="http://www.example.com/path/to/recaptcha/index.php#www.example.com/join_form"
style="height:200px;width:500px;border:none" ></iframe>
<!-- end edit -->

<div class="formControls">
<!-- start edit: add 'id="submitButton"' and 'style="display:none"' to the input tag -->
<input class="context"
id="submitButton"
style="display:none"
type="submit"
tabindex=""
name="form.button.Register"
value="Register"
i18n:attributes="value label_register;"
tal:attributes="tabindex tabindex/next;" />
<!-- end edit -->

</div>

</fieldset>

<input type="hidden" name="form.submitted" value="1" />
<!-- start edit: add this -->
<input id="passkey" type="hidden" name="passkey" value="" />
<!-- end edit -->

</form>
</div>

</body>
</html>

The crucial part in this script is to adapt the lines

if (lastId == "#YOUR PUBLIC KEY HERE")

and

src="http://www.example.com/path/to/recaptcha/index.php#www.example.com/join_form"

4. Customize /portal_skins/plone_login/join_form_validate

Edit the “Parameter List” field and add `,passkey=”`, so that the field contains

username='',email='',password='',password_confirm='',passkey=''

Insert before “if state.getErrors():”:

# recaptcha check
if not passkey or passkey != "#YOUR PUBLIC KEY":
context.plone_utils.addPortalMessage(_(u'Not allowed.'))
return state.set(status='failure')
# end recaptcha check

How it works:

  • The plone page loads an iframe from the same domain (or a different one – you can install one recaptcha script for different plone sites) with a PHP script. The button to submit the signup form is hidden.
  • The script displays the Captcha and submits the user input to the recaptcha.org server.
  • When the user has typed in the words correctly, the php script sends a message to the parent page which contains the iframe. Because of security limitations, we cannot access the parent frame directly (same-origin policy). We therefore manipulate the parent’s fragment identifier (as described here).
  • The parent page checks for the change in the fragment identifier and submits the form as soon as the fragment identifier is set to the public key. It also sets the value of a hidden form field to the public key.
  • The server-side script detects if the public key was submitted and otherwise refuses registration.

To do:

  • Passing the public key might not be a good idea, this should be done by generating a random value. The tricky part is to generate this value on the parent page, pass it to the iframe, get it back and then submit it to plone, which needs to know how to check it.
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s