I'm not sure if there already is something like that but I have no idea with what keywords can I find something like that, I failed to find on google.

However the problem is I keep seeing from time to time huge list of "user_register_form post blocked by CAPTCHA module:..." with 3 second intervals and up to ~20-30 attempts in a row. After that a new "bot" will be registered.

Is there anything to block/disable the ip/form after many fail captchas for say 5-10 min.

Thanks in advance.

Comments

Priority:Major» Normal

There is no such feature out of the box with the CAPTCHA module, as far as I know

as someone who experiences exactly the same problem on a daily basis, I'd love to see this feature implemented.

Bumpage. This feature needs to be created!

Totally useful feature! How else can I ban this bot which tries to use my website's webform and to register 100 times per day?

Agree, something to check logs or number of failed attempts and ban their ip...

Here is some code that I found, that basically gives a report of failed attempts.. If there is anyone good with php, they could probably change the line that turns the ip address in to a linkable field and sends you to what's my ip into a function that just adds that IP to the .htaccess file..

<?php
/* ==============================================================================
* System        : Drupal (Tested on Drupal 6)
* Author        : Grant at mmt.org
* Date Written  : December 2010
* Description   : This PHP code snippet produces several reports on how often
*                 your Drupal site gets attacked by spammers, where the attacks
*                 come from and which pages they are hitting most. It is designed
*                 to work as a stand-alone piece of code, but you could easily
*                 tweak it, style it or what have you.
* How to use it : Cut and paste this code into a page or other node, preferably
*                 one that is secure or even unpublished. You may want to remove
*                 a few HTML BR tags and the like if you want to use CSS styling.
* Dependencies  : Since this code pulls recent CAPTCHA stats from the logs it is
*                 dependent on your dblog being active and having enough entries
*                 and on you actually using CAPTCHA. It also makes use of jQuery,
*                 which is built in to Drupal.
* Disclaimers   : This code is designed to just be dropped into a page as a
*                 snippet and as such does not do everything "the Drupal way."
* Examples      : You can see examples of the reports from here:
*                 Overview and Summary: bit.ly/fnbs95
*                 Report by date: bit.ly/hHp1ng
*                 IP Address report: bit.ly/fckR8b
* ==============================================================================
*/
/**
* ------------------------------------------------------------------------------
* Fist show the user a standard Drupal error message if the DBLog module is
* not enabled.
* ------------------------------------------------------------------------------
*/
if (! module_exists('dblog')) { // If the DBLog module is not enabled
 
echo
 
drupal_set_message(
   
t('The DBLog module is not active and it is required for this snippet to function correctly. ' .
     
'Go to <a href="@module-page">the module list</a> and enable "Database logging" for this to report to work correctly',
      array(
'@module-page' => url('admin/build/modules'))),
   
'error', FALSE);
}
/**
* ------------------------------------------------------------------------------
* Pick up all logged Captcha blocks, latest first. If you'd like to search on
* more than just Mollom and CAPTCHA then simply add another entry to the IN
* clause in the SQL SELECT statement.
* ------------------------------------------------------------------------------
*/
$result  = db_query("
  SELECT variables, location, timestamp, hostname
    FROM {watchdog}
   WHERE type IN ('CAPTCHA', 'mollom')
   ORDER BY timestamp DESC"
);
/**
* ------------------------------------------------------------------------------
* Go through the log entries and accumulate totals by date, and within date, by
* form/page. Also accumulate counters by hostname for a second report.
* ------------------------------------------------------------------------------
*/
// Define some work variables.
$firstTimestamp  = $lastTimestamp  = $overallCounter  = 0;
$currentDate     = '';
$dateTotals      = $hostTotals  = $dailyOffenders  = $overallTotals  = array();
// Loop through the CAPTCHA log entries found.
while ($captchaBlock = db_fetch_array($result)) {
  if (
$lastTimestamp == 0) { // If this is the first row for a given date
   
$lastTimestamp  = $captchaBlock['timestamp'];
  }
 
// Save date and counter for later use
 
$startingFrom    = format_interval(time() - $captchaBlock['timestamp']); // Display version of earliest date
 
$firstTimestamp  = $captchaBlock['timestamp']; // Raw version of earliest date
 
$overallCounter++;
 
// Set up the base url, without the host, i.e. abc.com/xyz becomes /xyz
 
$afterHost   = strpos($captchaBlock['location'], $_SERVER['HTTP_HOST']) + strlen($_SERVER['HTTP_HOST']);
 
$pageSubUrl  = trim(substr($captchaBlock['location'], $afterHost));
 
// Get the form name.
 
$details  = unserialize($captchaBlock['variables']); // Extract the variable data
  // Set the identifier as either the form name or a page.
 
if (isset($details['%form_id'])) { // If we have a form name
    // Set up a user-readable title. In most cases we use the form id, but in some cases
    // we use the URL, because the name is likely to be more meaningful.
   
$title  = $details['%form_id']; // Default
   
if (substr($details['%form_id'], 0, 7) == 'webform') { // If the 1st 7 characters of the form id are 'webform'
     
if (substr($pageSubUrl, 1, 4) != 'node') { // If this is not a node URL
       
$title  = substr($pageSubUrl, 1);
      }
    }
   
// Turn the form name/alias into a user-readable title.
     
$formName  = ucwords(str_replace(array('_', '-', '/'), ' ', $title));
   
// If this is a webform then make it a clickable link. We don't do this with everything
    // as it is usually TMI, e.g. with comments being all over a site, you'd have to have a
    // line for each comment form. Also, making the user register page clickable is pointless
    // as that page is not viewable while logged in. If you want a clickable link for
    // everything, simply remove the if statement below and leave only the code inside the if.
    // Remove the whole if if you don't want this done for webforms either. And of course you
    // can add as many additional checks to the if as you like
   
if (substr($details['%form_id'], 0, 7) == 'webform') { // If this is a webform
     
$formName  = l($formName, substr($pageSubUrl, 1));
    }
  } else {
// If no form was specified then use the location (without the main url)
   
$formName  = l(t($pageSubUrl), substr($pageSubUrl, 1));
  }
 
// Set up a display date and use it to control the appearance of the report by date.
 
$shortDate  = format_date($captchaBlock['timestamp'], 'custom', "m/d/Y");
 
// Processing by date.
 
if ($shortDate == $currentDate) { // If we're processing an existing date
   
$dateTotals[$shortDate]['count']++;
    if (isset(
$dateTotals[$shortDate]['formList'][$formName])) {
     
$dateTotals[$shortDate]['formList'][$formName]++;
    } else {
     
$dateTotals[$shortDate]['formList'][$formName]  = 1;
    }
   
//$dailyOffenders[$captchaBlock['hostname']]++;
 
} else { // Otherwise, if this is a new date
   
$currentDate             = $shortDate; // Track the date
   
$dateTotals[$shortDate]  = array('count' => 1, 'formList' => array($formName => 1));
   
//$dailyOffenders = array();
 
}
 
// Track overall totals for a summary of what the attackers are hitting most
 
$overallTotals[$formName]++;
/**
* ------------------------------------------------------------------------------
* Build report of counters by hostname.
* ------------------------------------------------------------------------------
*/
  // Set up the conter index depending on the type of attack.
 
if (strpos($details['%form_id'], 'comment_form') !== false) { // If this is a comment
   
$addToCounter  = 'comment';
  } elseif (
strpos($details['%form_id'], 'user_register') !== false) { // If this is a webform
   
$addToCounter  = 'reg';
  } elseif (
substr($details['%form_id'], 0, 7) == 'webform') { // If this is a webform
   
$addToCounter  = 'webform';
  } else {
   
$addToCounter  = 'other';
  }
 
// If this is the first time processing an IP address then set up the array of counters.
 
if (! isset($hostTotals[$captchaBlock['hostname']])) {
   
$hostTotals[$captchaBlock['hostname']]  = array('count' => 0, 'reg' => 0, 'comment' => 0, 'webform' => 0, 'other' => 0);
  }
 
// Add to relevant counters for attacks from this IP address.
 
$hostTotals[$captchaBlock['hostname']]['count']++; // Totals for this host
 
$hostTotals[$captchaBlock['hostname']][$addToCounter]++; // Totals by attack
}
/**
* ------------------------------------------------------------------------------
* Go through the accumulated totals and build the primary report's detail.
* ------------------------------------------------------------------------------
*/
$captchaRows  = '';
foreach (
$dateTotals as $reportDate => $totals) {
 
$captchaRows  .= "<tbody>"; // Wrap each date cluster in its own table body
  // Set overall totals for the 1st 2 columns.
 
$firstTwoColumns  = "
        <td>
{$reportDate}</td>
        <td>"
. number_format($totals['count'], 0, '', ',') . "</td>";
 
// Produce a report line for each date+form combination, with
  // only the 1st line having date details. First sort the array
  // into alphabetical order by form, for consistency.
 
ksort($totals['formList']);
  foreach (
$totals['formList'] as $form => $formCount) {
   
$captchaRows  .= "
      <tr>
       
{$firstTwoColumns}
        <td>
{$form}</td>
        <td>
{$formCount}</td>
      </tr>"
;
   
// First 2 columns are blank for all but the first row.
   
$firstTwoColumns  = '
      <td>&nbsp;</td>
      <td>&nbsp;</td>'
;
  }
 
$captchaRows  .= '</tbody>'; // Close the date cluster's table body
}
/**
* ------------------------------------------------------------------------------
* Build some additional useful information to show the user.
* ------------------------------------------------------------------------------
*/
// Build a summary of where the greatest attck volume is.
$summaryHeader  =
   
t("Summary of %overall Recent Access Attempts Blocked by CAPTCHA in the last %start",
      array(
       
'%overall' => number_format($overallCounter, 0, '', ','),
       
'%start'   => $startingFrom
     
)
    );
$summaryReport  = "";
arsort($overallTotals); // Sort by the counter so that the worst offenders appear 1st
foreach ($overallTotals as $attackedForm => $numberOfAttacks) {
 
// Calculate a percentage.
 
$rawPercentage   = round ( $numberOfAttacks / ( $overallCounter / 100 ), 2 );
 
$nicePercentage  = number_format($rawPercentage, 0);
 
$summaryReport  .= "
    <tr>
      <td>
{$attackedForm}:</td><td>" . number_format($numberOfAttacks, 0, '', ',') . '</td><td>' . number_format($rawPercentage, 0) . '%</td>
    </tr>'
;
}
// Wrap the summary
$summaryReport  = "
    <table id='captcha_stats_summary' rules='groups' frame='box'>
      <thead>
        <tr>
            <th>"
. t('Form/Page')   . "</th>
            <th>"
. t('Attempts')    . "</th>
            <th>"
. t('Approximate %')  . "</th>
        </tr>
      </thead>
      <tbody>
         
{$summaryReport}
      </tbody>
    </table>"
;
// Collect some additional info on Captcha blocking, which is found in the Drupal
// variables table, and can be accessed with a standard Drupal function.
$captchaEngine    = variable_get('captcha_default_challenge', '**Unknown**');
$engineNameParts  = explode('/', $captchaEngine);
$totalSpamAttempts  = number_format(variable_get('captcha_wrong_response_counter', 0), 0, '', ',');
$sinceInception     =
 
t("Since being activated, CAPTCHA has stopped %attempts. The default captcha engine is currently %engine.",
    array(
     
'%attempts' => format_plural($totalSpamAttempts, '1 probable spam attempt', '@count probable spam attempts'),
     
'%engine'   => $engineNameParts['1']
    )
  );
// Set a link to help users tweak logging settings.
$changeLoggingSettings  =
 
t('If the report below shows you too much information or too little, then you might consider <a href="@settings-page">changing your logging settings</a>.',
    array(
'@settings-page' => url('admin/settings/logging/dblog'))
  );
// Set a link to an alternate way to view the log.
$viewLog  =
 
t('You can also <a href="@view-log-page">view recent log entries in a different way</a>.',
    array(
'@view-log-page' => url('admin/reports/dblog'))
  );
// Calculate the elapsed time covered and use it to calculate the average number
// of CAPTCHA blocks per day. In this case that is defined by
$timeDiffSeconds  = $lastTimestamp - $firstTimestamp;
$timeDiffDays     = (($timeDiffSeconds / 60) / 60 ) / 24;
$averagePerDay    = number_format($overallCounter / $timeDiffDays, 0, '', ',');
$displayAverage   =
 
t('During this period there were an average of %average-attempts by CAPTCHA every day.',
    array(
'%average-attempts' => format_plural($averagePerDay, '1 block', '@count blocks')));
/**
* ------------------------------------------------------------------------------
* Complete building the primary report.
* ------------------------------------------------------------------------------
*/
if ($overallCounter) { // If we found anything to report on
 
$mainReport  = "
    <p>
{$displayAverage}</p>
    <table id='captcha_stats_report' rules='groups' frame='box'>
      <thead>
        <tr>
            <th>"
. t('Date')       . "</th>
            <th>"
. t('Attempts')   . "</th>
            <th>"
. t('Form/Page')  . "</th>
            <th>"
. t('Attempts')   . "</th>
        </tr>
      </thead>
      <tbody>
         
{$captchaRows}
      </tbody>
    </table>"
;
 
$mainReportHeader  = t("Summary Broken Down By Date");
/**
* ------------------------------------------------------------------------------
* Build a secondary report on hostnames (IP addresses). Many rows are hidden by
* default based thresholds of failure and lines. Change the following two
* variables to see a or less rows.
* ------------------------------------------------------------------------------
*/
 
$blockThreshold  = 6// A threshold for which to hide rows with fewer CAPTCHA fails
 
$lineThreshold   = 10; // A threshold for which to hide less pesky spammers
 
$hostCnt         = 0;
 
$odd             = true;
 
$worstOffenders  = $otherOffenders  = '';
 
arsort($hostTotals); // Sort by the counter so that the worst offenders appear 1st
 
foreach ($hostTotals as $ipAddress => $ipTotals) {
   
$hostCnt++;
   
// Check to see if this IP address is already blocked from Drupal.
   
$ipBlocked  = db_fetch_array(db_query("
      SELECT aid
        FROM {access}
       WHERE `type`='host'
         AND `status`=0
         AND `mask`='
{$ipAddress}'
       LIMIT 1"
));
   
$blocked  = '';
    if (
$ipBlocked['aid']) { // If we have already blocked this ip from drupal
     
$blocked  = '(Blocked)';
    }
   
// If we've finished building the report of the worst offenders then close it out.
   
if (   ! $worstOffenders // If we have not already built the worst offender report
       
&& ($hostCnt >= $lineThreshold || $ipTotals['count'] < $blockThreshold)) { // and we're now processing less important hosts
     
$worstOffenders  = wrapHostReportTable($hostRows); // Build the table.
     
$hostRows        = ''; // Start from scratch for the 2nd report
   
}
   
// Do that thing Drupal does with odd and even rows, to aid CSS styling.
   
if($odd) {
     
$lineClass  = 'odd';
     
$even       = false;
    } else {
     
$lineClass  = 'even';
     
$even       = true;
    }
   
// We wrap the IP address in a link to an external service that will show you
    // details about the IP address. If you don't like this then remove the next
    // line, or feel free to make it link to a different service.
   
$ipAddressLink  = l($ipAddress, "http://whatismyipaddress.com/ip/{$ipAddress}", array('attributes' => array('target' => '_blank', 'class' => 'ExternalLink')));
   
$hostRows  .= "
      <tr
{$lineStyles} class='{$lineClass}{$lineHideClass}'>
        <td class='captcha_col_1'>
{$ipAddressLink}{$blocked}</td>
        <td class='captcha_col_2'>"
. number_format($ipTotals['count'], 0, '', ',') . "</td>
        <td class='captcha_col_3'>"
. number_format($ipTotals['reg'], 0, '', ',') . "</td>
        <td class='captcha_col_4'>"
. number_format($ipTotals['comment'], 0, '', ',') . "</td>
        <td class='captcha_col_5'>"
. number_format($ipTotals['webform'], 0, '', ',') . "</td>
        <td class='captcha_col_6'>"
. number_format($ipTotals['other'], 0, '', ',') . "</td>
      </tr>"
;
  }
 
// If we had enough spammers for 2 reports, then set up the second. The rest
  // are in a hidden DIV that can be revealed or re-hidden by clicking on a simple
  // jQuery toggle link. We do this to shorten what can be a very long list.
 
if ($worstOffenders) { // If we have set up the 1st report
   
if ($hostRows) { // If we have more rows to show
     
$otherOffenders  = wrapHostReportTable($hostRows);
     
$showMoreLink    = l(t('Show or Hide More Hosts'), $_REQUEST['q'], array('fragment' => 'hostRept', 'attributes' => array('name' => 'hostRept', 'onclick' => "$('#host_report_other').toggle('slow');")));
    }
  } else {
// If we have not closed out the 1st report yet
   
if ($hostRows) { // If we have rows to show
     
$worstOffenders  = wrapHostReportTable($hostRows);
     
$showMoreLink    = '';
    }
  }
 
$hostReport  = "
    <div id='host_report_container'>
      <div id='host_report_worst'>
         
{$worstOffenders}
      </div>
      <br />
     
{$showMoreLink}
      <div id='host_report_other' style='display: none;'>
         
{$otherOffenders}
      </div>
    </div>"
;
 
$hostReportHeader  = t('Spammer Hosts, Starting With The Worst Offenders.');
 
$hostReportText    = t('The following list shows up to @line-threshold potential spammers with at least @block-threshold form submissions blocked by CAPTCHA. ' .
                        
'Click on the Show/Hide link to see all the IP Addresses. Click on any IP address to find out more about them. Those marked as blocked have ' .
                        
'been <a href="@access-page">blocked from Drupal</a> (it\'s better to get your sysadmin to block them, but it gets the job done).',
                         array(
'@line-threshold' => $lineThreshold, '@block-threshold' => $blockThreshold, '@access-page' => url('admin/user/rules/list')));
} else {
// If we found nothing to report on
 
$mainReportHeader  = t('Unable to find any CAPTCHA or Mollom entries in your watchdog tables.');
 
$mainReport        = $hostReport  = $hostReportHeader = '';
}
$mainHeader  t('Overview of CAPTCHA Spam Blocking');
/**
* ------------------------------------------------------------------------------
* Output the final reports. In a perfect world this would be in the theme layer.
* ------------------------------------------------------------------------------
*/
echo "
  <div id='captcha_stats_container'>
    <h3>
     
{$mainHeader}
    </h3>
    <p>
     
{$sinceInception} {$changeLoggingSettings} {$viewLog}
    </p>
    <br />
    <h3>
     
{$summaryHeader}
    </h3>
   
{$summaryReport}
    <br />
    <h3>
     
{$mainReportHeader}
    </h3>
   
{$mainReport}
    <br />
    <h3>
     
{$hostReportHeader}
    </h3>
    <p>
     
{$hostReportText}
    </p>
   
{$hostReport}
  </div>
  <br />"
;
/**
* ------------------------------------------------------------------------------
* ============================================ Functions used by the above code.
* ------------------------------------------------------------------------------
*/
/**
* ------------------------------------------------------------------------------
* Function to wrap some table lines in a header.
* ------------------------------------------------------------------------------
* @param string $pLines - The lines to be wrapped in a table.
* @return string with HTML table
*/
function wrapHostReportTable($pLines) {
  return
"
    <table class='host_stats_report' rules='groups' frame='box'>
      <thead>
        <tr>
            <th colspan='2'>"
. t('Main')       . "</th>
            <th colspan='4'>"
. t('Breakdown')  . "</th>
        </tr>
        <tr>
            <th>"
. t('IP Address')   . "</th>
            <th>"
. t('Attempts')     . "</th>
            <th>"
. t('Registration') . "</th>
            <th>"
. t('Comments')     . "</th>
            <th>"
. t('Webforms')     . "</th>
            <th>"
. t('Other')        . "</th>
        </tr>
      </thead>
      <tbody>
         
{$pLines}
      </tbody>
    </table>"
;
}
?>

The code I use right now inconjunction with the Honeypot module to autoban IP's.. If this could be turned into a function and inserted into the code above so you could just browse the report and click on them and their gone that would be cool.. Or if someone could dechiper the code above and put allow a variable of X attempts to be performed and the the the code below is executed that would be even better

<?php
$filename
= '.htaccess'; // CHANGE IF NECESSARY
if ($fp = fopen($filename, 'a')) {
      if (
flock($fp, LOCK_EX)) {
           
$remote_addr = $_SERVER['REMOTE_ADDR'];
           
fwrite($fp, "deny from $remote_addr\n");
      }
      
flock($fp, LOCK_UN);
     
fclose($fp);
}
header('HTTP/1.1 403 Forbidden');
echo
"Forbidden!"; // Add any other HTML here
exit();
// EOF
?>

StatusFileSize
new0 bytes

I need this Feature too.
I've created a patch which blocks a IP, if there are more then 5 Attemps. Use this patch carefully, its my first Drupal patch.

There is a 0 Byte file, dont know why. This is my Patch:

diff --git a/captcha.module b/captcha.module.patched
index 5107452..5a00fe4 100644
--- a/captcha.module
+++ b/captcha.module.patched
@@ -610,6 +610,23 @@ function captcha_validate($element, &$form_state) {
         ->condition('csid', $csid)
         ->expression('attempts', 'attempts + 1')
         ->execute();
+
+         $attempts_and_ip = db_query(
+               'SELECT attempts,ip_address FROM {captcha_sessions} WHERE csid = :csid',
+               array(':csid' => $csid)
+               )
+               ->fetchAssoc();
+         $max_attempts=5;
+               // Ban IP if it has enter the Wrong Captcha for $max_attempts Times
+
+         if ($attempts_and_ip['attempts']>=$max_attempts){
+                       db_insert('blocked_ips')
+                       ->fields(array('ip' => $attempts_and_ip['ip_address']))
+                       ->execute();
+                       watchdog('CAPTCHA',t('IP Adress %ip_address has been Blocked by CAPTCHA. Because of exceeding the Max Wrong Captcha Input of %maxattempts times'),array('%ip_address'=>$attempts_and_ip['ip_address'],'%maxattempts'=>$max_attempts));
+                       form_set_error('captcha_response', t('Yout IP has been Blocked. Please dont SPAM!'));
+               }
+

This would be very useful, and the patch seems like a promising start.

StatusFileSize
new1.28 KB

Here's an updated and perhaps slightly better formatted version.

TODO:
* Make max attempts configurable
* Do we need to expire old attempts or reset to 0 on success?

Thanks for your patch. But my patch doesn't work well :(
It requires that the SPAMER always using the same csid , but it seems like that there tools refresh the page after each try, so there get a new csid and then this patch doesnt work.