09 Retro's Anti Flood System

Started by Mindless, July 20, 2012, 08:57:12 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Mindless

#1
Credits to Retro.
Massive thanks to pdq and putyn for assistance with the page_verify class.
Xhtml Vaild.

::NOTE:: Experience level required: INTERMEDIATE...

The replacement anti flood code for my original modification submitted some time ago. This code has been written to be compatible with Tbdev 09 Final with 09 Multilayer forum, i need a default install forum to do the edits for that.

This new version does not rely on the clock being accurate, but works mainly from the cleanup routine.

It adds one single query per cleanup, yet offers excellent protection against someone flooding your PM, Post or Torrent Comment system.

First things first, we need to declare six new columns in the 'users' table.
Code (sql) Select
ALTER TABLE `users` ADD `pm_max` TINYINT( 3 ) UNSIGNED DEFAULT '20' NOT NULL AFTER `last_access` ,
ADD `pm_count` TINYINT( 3 ) UNSIGNED DEFAULT '0' NOT NULL AFTER `pm_max` ,
ADD `post_max` TINYINT( 3 ) UNSIGNED DEFAULT '20' NOT NULL AFTER `pm_count` ,
ADD `post_count` TINYINT( 3 ) UNSIGNED DEFAULT '0' NOT NULL AFTER `post_max` ,
ADD `comment_max` TINYINT( 3 ) UNSIGNED DEFAULT '20' NOT NULL AFTER `post_count` ,
ADD `comment_count` TINYINT( 3 ) UNSIGNED DEFAULT '0' NOT NULL AFTER `comment_max`;



[cleanup.php]

Open cleanup.php and insert the following two lines before the final closing brace...

Code (php) Select
// Reduce Counters for all user rows
sql_query("UPDATE users SET pm_count = if( pm_count > 1, pm_count -2, pm_count ) , post_count = if( post_count > 1, post_count -2, post_count ), comment_count = if( comment_count > 1, comment_count -2, comment_count )") or sqlerr(__FILE__, __LINE__);




Let me explain what this does... Since the new columns are defined as unsigned ints, the minimum they can contain is 0, and the maximum is 255. If you try to enter a negative, it will default to 0, and if you try to enter a number greater than 255, it will default to 255.

The _count fields will contain the current counter for each attribute (self explanatory). The line of code in cleanup.php will reduce the _count columns by 2, regardless of the contents, at every cleanup stage.

::NOTE:: My cleanup is set to every 20 minutes, so the _count is reduced by 6 per hour. The standard cleanup is normally run every 15 minutes, so this will reduce _count by 8 per hour.

That's the automated part out of the way...

[userdetails.php]

Open userdetails and locate the following lines, near the end...
Code (php) Select
$HTMLOUT .= "<tr><td colspan='3' align='center'><input type='submit' class='btn' value='{$lang['userdetails_okay']}' /></td></tr>\n";


and above it add ...
Code (php) Select
$HTMLOUT .="<tr><td class='rowhead'>PM Limit</td><td colspan='2' align='left'>".
"<input type='text'  maxlength='3' size='3' name='pm_max' value=\"".htmlspecialchars($user['pm_max']).
"\" /> (Max 255)  -  CCVal: ".htmlspecialchars($user['pm_count'])."</td></tr>\n";

$HTMLOUT .="<tr><td class='rowhead'>Post Limit</td><td colspan='2' align='left'>".
"<input type='text'  maxlength='3' size='3' name='post_max' value=\"".htmlspecialchars($user['post_max']).
"\" /> (Max 255)  -  CCVal: ".htmlspecialchars($user['post_count'])."</td></tr>\n";

$HTMLOUT .="<tr><td class='rowhead'>Comment Limit</td><td colspan='2' align='left'>".
"<input type='text'  maxlength='3' size='3' name='comment_max' value=\"".htmlspecialchars($user['comment_max']).
"\" /> (Max 255)  -  CCVal: ".htmlspecialchars($user['comment_count'])."</td></tr>\n";



[modtask.php]

Then find the lines...
Code (php) Select
$updateset = $useredit['update'] = array();

  $modcomment = (isset($_POST['modcomment']) && $CURUSER['class'] == UC_MAX) ? $_POST['modcomment'] : $user['modcomment'];



Under it add :
Code (php) Select
////////////////////////Retro's 09 flood protection/////////
    // Set Post Limit
    if (isset($_POST['post_max'])) {
        $post_max = 0 + $_POST['post_max'];
        $curpost_max = $user['post_max'];

        if (($post_max < 0) or ($post_max > 255))
            $post_max = 20; // Default number

        if ($post_max != $curpost_max) {
            $modcomment = get_date( time(), 'DATE', 1 ) . " - Post Limit changed from " . $curpost_max .
                " to " . $post_max . " by " . $CURUSER['username'] . ".\n" . $modcomment;
            $updateset[] = "post_max = " . sqlesc($post_max);
        }
    }
    // Set Comment Limit
    if (isset($_POST['comment_max'])) {
        $comment_max = 0 + $_POST['comment_max'];
        $curcomment_max = $user['comment_max'];

        if (($comment_max < 0) or ($comment_max > 255))
            $comment_max = 20; // Default number

        if ($comment_max != $curcomment_max) {
            $modcomment = get_date( time(), 'DATE', 1 ) . " - Comment Limit changed from " . $curcomment_max .
                " to " . $comment_max . " by " . $CURUSER['username'] . ".\n" . $modcomment;
            $updateset[] = "comment_max = " . sqlesc($comment_max);
        }
    }
    // Set PM Limit
    if (isset($_POST['pm_max'])) {
        $pm_max = 0 + $_POST['pm_max'];
        $curpm_max = $user['pm_max'];

        if (($pm_max < 0) or ($pm_max > 255))
            $pm_max = 20; // Default number

        if ($pm_max != $curpm_max) {
            $modcomment = get_date( time(), 'DATE', 1 ) . " - PM Limit changed from " . $curpm_max .
                " to " . $pm_max . " by " . $CURUSER['username'] . ".\n" . $modcomment;
            $updateset[] = "pm_max = " . sqlesc($pm_max);
        }
    }




This is the interface out of the way. You can now change the max values for each attribute. You may want to change a value because someone may need to submit more, such as a First Line Support who mainly deals with PMs, or you may wish to restrict, such as a forum spammer.

By setting these values to zero, you effectively cut off their ability to post, Comment or PM.


[comment.php]

Open comment.php and change the lines...

Code (php) Select
if ($action == 'add') {
       
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {

        $id = (isset($_POST['tid']) ? $_POST['tid'] : 0);



to...
Code (php) Select
if ($action == 'add') {
       
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
     //== Anti Flood Code

if (!($CURUSER['comment_count'] < $CURUSER['comment_max']))
stderr('Notice','You have reached your Comment limit. Please wait 15 minutes before retrying.');
       
        $id = (isset($_POST['tid']) ? $_POST['tid'] : 0);




and change...
Code (php) Select
sql_query("UPDATE torrents SET comments = comments + 1 WHERE id = $torrentid");


to...
Code (php) Select
sql_query("UPDATE torrents SET comments = comments + 1 WHERE id = $torrentid");
     
      // Update Last Comment sent...
    sql_query("UPDATE users SET comment_count = comment_count + 1 WHERE id = ".sqlesc($CURUSER['id'])) or sqlerr(__FILE__, __LINE__);



[details.php]

Open details.php and change...
Code (php) Select
$HTMLOUT .= "<p><a name=\"startcomments\"></a></p>\n";
$commentbar = "<p align='center'><a class='index' href='comment.php?action=add&amp;tid=$id'>{$lang['details_add_comment']}</a></p>\n";



to...
Code (php) Select
$HTMLOUT .= "<p><a name=\"startcomments\"></a></p>\n"
$postallowed = 1;
if ($CURUSER['comment_max'] == 0)
$postallowed = 0;
if ($postallowed AND (!($CURUSER['comment_count'] < $CURUSER['comment_max'])))
$postallowed = 2;

switch ($postallowed) {
case 0:
$commentbar = "<p align='center'>Your posting privilege has been revoked!</p>\n";
break;
case 1:
$commentbar = "<p align='center'><a class='index' href='comment.php?action=add&amp;tid=$id'>Add a comment</a></p>\n";
break;
case 2:
$commentbar = "<p align='center'>You have reached your Comment limit. Please wait 15 minutes before retrying.</p>\n";
default:
die('Contact Administrator');
break;
}



::NOTE:: The 15 minute interval before retrying is based on the standard cleanup period. If yours is different, alter the text to reflect this.

Submitting comments is now controlled by the counter...

[takemessage.php]

Open takemessage.php and change...
Code (php) Select
else
{                        //////  PM  ///
$receiver = isset($_POST["receiver"]) ? $_POST["receiver"] : false;
$origmsg = isset($_POST["origmsg"]) ? $_POST["origmsg"] : false;
$save = isset($_POST["save"]) ? $_POST["save"] : false;
$returnto = isset($_POST["returnto"]) ? $_POST["returnto"] : '';



to...
Code (php) Select
else
{   
  // Anti Flood Code
  // This code restricts PM sending to a set limit
  if (!($CURUSER['pm_count'] < $CURUSER['pm_max']))
  stderr('Notice','You have reached your PM limit. Please wait 15 minutes before retrying.');
                                          //////  PM  ///
$receiver = isset($_POST["receiver"]) ? $_POST["receiver"] : false;
$origmsg = isset($_POST["origmsg"]) ? $_POST["origmsg"] : false;
$save = isset($_POST["save"]) ? $_POST["save"] : false;
$returnto = isset($_POST["returnto"]) ? $_POST["returnto"] : '';



and change...

Code (php) Select
$subject = trim($_POST['subject']);
   
sql_query("INSERT INTO messages (poster, sender, receiver, added, msg, subject, saved, location) VALUES(" . $CURUSER["id"] . ", " . $CURUSER["id"] . ", $receiver, " . time() . ", " . sqlesc($msg) . ", " . sqlesc($subject) . ", " . sqlesc($save) . ", 1)") or sqlerr(__FILE__, __LINE__);
   
if (strpos($user['notifs'], '[pm]') !== false)



to...
Code (php) Select
sql_query("INSERT INTO messages (poster, sender, receiver, added, msg, subject, saved, location) VALUES(" . $CURUSER["id"] . ", " . $CURUSER["id"] . ", $receiver, " . time() . ", " . sqlesc($msg) . ", " . sqlesc($subject) . ", " . sqlesc($save) . ", 1)") or sqlerr(__FILE__, __LINE__);
//== Update Last PM sent...
sql_query("UPDATE users SET pm_count = pm_count + 1 WHERE id = ".sqlesc($CURUSER['id'])) or sqlerr(__FILE__, __LINE__);
if (strpos($user['notifs'], '[pm]') !== false



[sendmessage.php]

Open sendmessage.php and change...
Code (php) Select
}
    else
    {                                                        ////////  PM  //
      $receiver = 0+$_GET["receiver"];
      if (!is_valid_id($receiver))
        die;



to...
Code (php) Select
}
    else
    {                                                       
    if ($CURUSER['pm_max'] == 0) stderr("System Message","You PM rights have been revoked.");
  // Anti Flood Code
// This code restricts PM sending to a set limit
if (!($CURUSER['pm_count'] < $CURUSER['pm_max']))
  stderr('Notice','You have reached your PM limit. Please wait 15 minutes before retrying.');
    ////////  PM  //
    $receiver = 0+$_GET["receiver"];
    if (!is_valid_id($receiver))
     die;



These changes will stop someone from sending a PM using the sendmessage interface, or directly with the takemessage.php script.

Lastly, the forums code...

[forums.php]

Find 2 instances of this :
Code (php) Select
if (!is_valid_id($forumid))
            stderr('Error', 'Invalid ID!');



Change them to...
Code (php) Select
if (!is_valid_id($forumid))
            stderr('Error', 'Invalid ID!');
// Anti Flood Code
    if (!($CURUSER['post_count'] < $CURUSER['post_max']))
stderr('Notice','You have reached your Post limit. Please wait 15 minutes before retrying.');




and change...
Code (php) Select
//------ Update topic last post

update_topic_last_post($topicid);



to...
Code (php) Select
//------ Update topic last post

update_topic_last_post($topicid);

// Update last post sent
sql_query("UPDATE users SET post_count = post_count + 1 WHERE id = ".sqlesc($CURUSER['id'])) or sqlerr(__FILE__, __LINE__);



Change this :
Code (php) Select
$HTMLOUT .= update_topic_last_post($topicid);


To
Code (php) Select
$HTMLOUT .= update_topic_last_post($topicid);
//== Update last post sent
sql_query("UPDATE users SET post_count = post_count + 1 WHERE id = ".sqlesc($CURUSER['id'])) or sqlerr(__FILE__, __LINE__);



Lastly, change...
Code (php) Select
} else {
        $arr = get_forum_access_levels($forumid);

        if ($CURUSER['class'] < $arr["write"]) {

          $HTMLOUT .="<p align='center'><i>You are not permitted to post in this forum.</i></p>";
         
            $maypost = false;
        } else
            $maypost = true;
    }



to..
Code (php) Select
} else {
        $arr = get_forum_access_levels($forumid);

        if ($CURUSER['class'] < $arr["write"]) {

          $HTMLOUT .="<p align='center'><i>You are not permitted to post in this forum.</i></p>";
         
          if ($CURUSER['post_max'] == 0)
      $HTMLOUT .="<p><i>Your posting privilege has been revoked.</i></p>\n";
   
    if (!($CURUSER['post_count'] < $CURUSER['post_max']))
  $HTMLOUT .="<p><i>You have reached your posting limit. Please retry in 15 minutes.</i></p>\n";
         
            $maypost = false;
        } else
            $maypost = true;
    }



I believe that I have covered all aspects of this modification. My own tracker, where this mod works, is very much different from default Tbdev 09, so I have not been able to test this properly using a vanilla Tbdev 09 source.


I would suggest, when installing this mod, to make changes to cleanup and userdetails/modtask, and make sure that the updating occurs properly. Once this is done, mod the comment/details code, then make a couple of comments to ensure that the counters are being updated. Once you are certain that the counters work, wait for cleanup to decrement the counters. Once this is done, lower the comment_max to about 3, then attempt to make four comments in a row, you should hit the barrier after the third comment.

Once you are happy with the Comment code, repeat this process with the PM and Post code. If you are not happy with any part, do not continue the installation until you have found out what the issue is.


If your still interested in the page_verify function, the following code which is still experimental, may be of use...

Save and upload page_verify.php to /include folder :
Code (php) Select
<?php
/**
 *   https://09source.kicks-ass.net:8443/svn/installer09/
 *   Licence Info: GPL
 *   Copyright (C) 2010 Installer09 v.2
 *   A bittorrent tracker source based on TBDev.net/tbsource/bytemonsoon.
 *   Project Leaders: Mindless,putyn,kidvision.
 **/
// session so that repeated access of this page cannot happen without the calling script.
//
// You use the create function with the sending script, and the check function with the
// receiving script...
//
// You need to pass the value of $task from the calling script to the receiving script. While
// this may appear dangerous, it still only allows a one shot at the receiving script, which
// effectively stops flooding.
// page verify by retro

      
      
class page_verify
      
{
      function 
page_verify ()
      {
      if (
session_id () == '')
      {
      
session_start ();
      }
      }
    
      function 
create ($task_name 'Default')
      {
      global 
$CURUSER;
      
$_SESSION['Task_Time'] = time ();
      
$_SESSION['Task'] = md5('user_id:' $CURUSER['id'] . '::taskname-' $task_name '::' $_SESSION['Task_Time']);
      
$_SESSION['HTTP_USER_AGENT'] = $_SERVER['HTTP_USER_AGENT'];
      }
      
      function 
check ($task_name 'Default')
      {
      global 
$CURUSER$INSTALLER09$lang
      
$returl = (isset($_SERVER['HTTP_REFERER'])?htmlspecialchars($_SERVER['HTTP_REFERER']):$INSTALLER09['baseurl']."/login.php"); 
      
$returl str_replace('&amp;''&'$returl); 
      if (isset(
$_SESSION['HTTP_USER_AGENT']) && $_SESSION['HTTP_USER_AGENT'] != $_SERVER['HTTP_USER_AGENT'])
      
stderr("Error""Please resubmit the form. <a href='".$returl."'>Click HERE</a>",false);
      if (
$_SESSION['Task'] != md5('user_id:' $CURUSER['id'] . '::taskname-' $task_name '::' $_SESSION['Task_Time']))
      
stderr("Error""Please resubmit the form. <a href='".$returl."'>Click HERE</a>",false);
      
$this->create ();
      }
      }
      
?>



Now usage of this is pretty straight forward i'll show one example on userdetails.php and modtask.php - then you can do edit.php/takeedit.php - my.php/takeprofiledit.php as its the same only difference being name of script

In userdetails.php find :
Code (php) Select
require_once(INCL_DIR.'user_functions.php');


Under it add
Code (php) Select
require_once(INCL_DIR.'page_verify.php');


Find :
Code (php) Select
$lang = array_merge( load_language('global'), load_language('userdetails') );


Under it add :
Code (php) Select
$newpage = new page_verify();
$newpage->create('modtask');



Now to modtask.php
Code (php) Select
require_once(INCL_DIR.'user_functions.php');


Under it add
Code (php) Select
require_once(INCL_DIR.'page_verify.php');


Find :
Code (php) Select
$lang = array_merge( load_language('global'), load_language('userdetails') );


Under it add :
Code (php) Select
$newpage = new page_verify();
$newpage->check('modtask');



So for you to use it on my/takeprofileedit.php the code placement is exact same only difference will be
Code (php) Select
$newpage = new page_verify();
$newpage->create('takeprofileedit');



And
Code (php) Select
$newpage = new page_verify();
$newpage->check('takeprofileedit');



for you to use it on edit/takeedit.php the code placement is exact same only difference will be
Code (php) Select
$newpage = new page_verify();
$newpage->create('takeedit');



And
Code (php) Select
$newpage = new page_verify();
$newpage->check('takeedit');



Only change is script thats being paired and checked :)

It is not compatible with the existing page_verify class, so if you install it on a system that already uses the older one, then rename this one to page_verify_2 or something similar...

What this one does is to create a keyed array in the $_SESSIONS variable, and to supply that key to the sender script. This is then passed to the receiver script as part of the query string or $_POST, whereby it is compared with the keyed value, and the entry removed, or a forced die() depending on whether they key validated.

The code is designed not to allow more than 10 keys, so a new key will wipe the first key stored. This will technically allow 10 tabbed pages in firefox or 10 browsers in IE. No one should really need more than that.

There is a downside to this code though. Unlike the old class, which can hide the taskname from the member, this one must provide the taskname, so that the checking code knows which key to check. This means that while the class will stop flooding of a take*.php script, it won't stop a single shot attack, using a modified sender script and a fresh taskname, discernable from the source...

With reference to the last post issue, where someone uses the BACK button to return to the COMPOSE page, and sends a second post, you can always run a pre-update query that simply pulls the last userid to post in that topic, and if it matches, bork(). This may require one additional query, but it's not like you have thousands of members continually posting simultaneously, unlike announce where you have thousands of members continually announcing, so you may be able to get away with this simple query.

I do believe that the torrent world as a whole has become complacent, and with all the recent hacks going on, has placed site security at the top of the list, where it bloody well should be...