Php page functionality spam prevention

Background: Ok, I run an old BBG at ninjawars.net. There is an "attack" that players can do on other players, which are initialized via the mail form. Basically we can simplify things to pretend there is a page, lets call it a .php attack with a giant "ATTACK" form message that is sent to another php page, lets call it accept_attack.php and the second page does the attack functionality say killing another player 1, 2 or 3. Server runs PHP5, Postgresql, Apache

Problems:

  • If I click on that big "ATTACK" button and it takes me to accept_attack.php, I can hit refresh three times, repeating each time to attack again three times in a row.
  • If I open three tabs on the first page and hit attack on each page, I get three instant attacks that kill players 1, 2, and 3 right away, and I can just constantly refresh to retry.
  • Despite my attempts to set a "last attack" timer to be stored in the database, players seem to be able to get around it, perhaps by simply updating the three copied tabs in a sufficiently synchronized way that they can all get the same timer (e.g. 10: 00: 00: 0000 am) and thus continue processing.

Solution needed:

So how can I prevent it from preparing the same processing of some script in triplicate at the same time?

Php, social engineering and / or javascript / jQuery solutions are preferred (probably in about that order).

Edit: Based on the answers, here's what I did (potentially, before stress testing): The session response seemed to be the simplest / most straightforward to implement, so I used this datastore. I tested it and it seems to work, but there may be ways around it that I am not aware of.

$recent_attack = null;
$start_of_attack = microtime(true);
$attack_spacing = 0.2; // fraction of a second
if(SESSION::is_set('recent_attack')){
    $recent_attack = SESSION::get('recent_attack');
}

if($recent_attack && $recent_attack>($start_of_attack-$attack_spacing)){
    echo "<p>Even the best of ninjas cannot attack that quickly.</p>";
    echo "<a href='attack_player.php'>Return to combat</a>";
    SESSION::set('recent_attack', $start_of_attack);
    die();
} else {
    SESSION::set('recent_attack', $start_of_attack);
}

      

If there are ways to improve this, or ways that it can be used (except for one obvious to me that echo stuff is not a good section of logic, I'd love to know. Along these lines, wiki ed.

+2


source to share


5 answers


Similar to Godecke's solutions. Could you create a token with a hidden field in the form of an Attack button and store it in the session? Then on the accept-attack.php page you have to check if there is $ _POST ['token'] == $ _SESSION ['token'].

so you have something similar in the accept-attack.php page

if($_POST['token'] == $_SESSION['token']){



       echo 'no cheating!';
            // or redirect to the attach page
   }else{
         $_SESSION['token'] = $_POST['token'];
         // then perform the attack
   } 

      

+3


source


While the womp Post-Redirect-Get pattern will address some issues if they deliberately play with the submission process, I doubt it will prevent the issue other than against lazy ones (as pointed out in the linked article, submissions up to a 302 response would be multiple, since the redirect hasn't happened yet).

Instead, you are probably better off putting some token of information on the attack page, which is not easy to replicate. When you accept the attack, push the attack to the database queue table. Specifically, save the information token sent to the attack page when queued and check if this token has already been used before the attack queue.

A simple source of tokens will be the results of running a random number generator and placing them in a table. Pull the next number for each attack page load, and make sure that number has been circulated recently. You can refill the tokens when loading attack pages and expire any "unused" tokens based on your policy on how long a page must be available before becoming "obsolete".



This way you create a finite set of "correct" tokens, you publish these tokens on the attack page (one per page), and you verify that their token has not yet been used on the attack handling page. To create repeated attacks, the player will need to determine which tokens are valid ... repeating the same message will fail because the token has been used. Use BigInt and a decent pseudo-random number generator and the search space makes it unlikely to be easily circumvented. (Note: you will need a transaction around token validation and refresh to ensure success with this method.)

If you have user accounts that require login, you can create and store those tokens in the user table (again, using a database transaction wrapped around these steps). Then each user will have one valid token at a time and multiple views will be caught in a similar manner.

+7


source


You can avoid most form re-submissions by using the Post-Redirect-Get pattern for messages on a form.

In short, instead of returning attack_accept.php

from the original post, return a 302 response to redirect the browser to attack_accept.php

. Now when the user reloads the page they just reload the 302 request and there is no form resubmission.

+4


source


Another solution would be to serialize the post data (like the JSON itself) and then hash it, storing the result in the DB.

if the user submits the same information twice, the hash will exist in the database.

you must also add a timestamp to the same table so the hashes can be deleted / updated after X hours

Php pseudo code example:

$hash = sha1(json_encode($_POST));
$results = $db->exec('SELECT timestamp FROM user_posts WHERE user_id=? AND hash=?', $user_id, $hash);

if ($results != null) {
    // check timestamp, allow if over 24 hours ago
    $ok = ($results['timestamp']+3600*24) < now();
} else {
    // no results, allow
    $ok = true;
}

if ($ok) {
    $db->exec('INSERT INTO user_posts (hash, timestamp) VALUES (?, ?)', $hash, now() );
} else {
    // show error page
    echo "your request has been denied!";
}

      

Note: this will still allow various POST data to be transmitted in a short amount of time, but it's also pretty easy to check.

+1


source


This solution should be impossible to get around:

1) Add a column " NextAttackToken CHAR(32)

" to your players table and give each player a randomly generated MD5 value.

2) On the page, attack.php

add a hidden current_token field with the player's current token.

3) On the page, accept_attack.php

use the following logic to determine if the player is actually allowed to attack:

// generate a new random token
$newToken = md5(microtime(true).rand());

// player is spamming if he has attacked less than 30 seconds ago
$maxTimer = date('Y-m-d H:i:s', strtotime('-30 seconds'));

// this update will only work if the player is allowed to attack
$query = "UPDATE player SET NextAttackToken = '$newToken'
               WHERE PlayerID = $_SESSION[PlayerID]
               AND PlayerLastAttack < '$maxTimer'
               AND NextAttackToken = '$_GET[current_token])'
         ";
$result = mysql_query($query);
if(mysql_affected_rows($result)) {
    echo "Player is allowed to attack\n";
}
else {
    echo "Player is spamming! Invalid token or submitted too soon.\n";
}

      

This solution works because mysql can only perform one UPDATE on a table at a time, and even if 100 spam queries are requested at the same time, the first mysql UPDATE will change the token and stop the other 99 updates from affecting any rows.

+1


source







All Articles