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.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

soxofaan’s picture

Priority: Major » Normal

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

omnyx’s picture

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

tekcert’s picture

Bumpage. This feature needs to be created!

pipicom’s picture

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?

zeroyon’s picture

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

zeroyon’s picture

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..


/* ==============================================================================
* 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

$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

Nightwalker3000’s picture

FileSize
0 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!'));
+               }
+

opoplawski’s picture

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

opoplawski’s picture

FileSize
1.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?

Nightwalker3000’s picture

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.

howdytom’s picture

opoplawski, thank you so much for sharing your patch!

Auto Block is a must-have feature! I am seeing hundreds of daily spam submissions that try to bypass Drupal Captcha. Yes, we do need auto expire would be useful. We should increase the default value for $max_attempts to 6 instead.

howdytom’s picture

Issue summary: View changes

As Nightwalker3000 alresdy pointed out, this patch is wonderful as long as the spammer uses the same csid. Unfortunately automated bot somehow refresh the csid on every reload. Any solutions?

Nightwalker3000’s picture

It looks like that this Module doesn’t get much love.
I took an another way and installed autoban. It supports Rules to monitor the watchdog and automatically Ban IP addresses based on failed captcha attempts.

howdytom’s picture

Thanks, autoban is pretty much what I was looking for. It provides a wide range of blocking options. However you have to enable Force mode in order to efficiently block spammers immediately. Force mode can slows down site loading and increases dramatically memory consumption.

Anybody’s picture

Status: Active » Closed (won't fix)

We won't fix this for Drupal 7, but I still like the idea to provide a CAPTCHA submodule which adds the user to the flood table (just like X failed login attempts - but for CAPTCHA and with a different (typically higher) limit).

Let's create a separate feature request issue for that.