最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Preventing spamming of the functionality of a php page - Stack Overflow

programmeradmin1浏览0评论

The background: Ok, I run a legacy BBG over at ninjawars. There is an "attack" that players can make on other players that is initialized via form post. Essentially, we can simplify the situation to pretend that there's a page, lets call it attack.php, with a giant "ATTACK" form post that submits to another php page, lets call it accept_attack.php, and the second page performs the attack functionality, lets say killing other player 1, 2, or 3. The server runs PHP5, Postgresql, Apache

The problems:

  • If I hit that big "ATTACK" button, and it brings me to the accept_attack.php, I can then hit refresh three times, resubmitting each time, to attack again three times in succession.
  • If I open up three tabs of the first page, and hit attack on each page, I end up with three instantaneous attacks that kill players 1, 2, and 3 all at once, and I can just continually refresh to repeat.
  • Despite my attempts to have a "most recent attack" timer that gets saved to the database, players seem to be able to work around it, perhaps just by refreshing three copied tabs in a synchronized enough way, so that they may all retrieve the same timer (e.g. 10:00:00:0000 am) and thus proceed with the resulting processing.

The solution needed:

So how do I prevent the same processing of a certain script from being preformed all at once in triplicate?

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

Edit: Based on the answers, here's what I did to (potentially, before stress testing) solve it: The session answer seemed simplest/most prehensible to implement, so I used that data store. I tested it and it seems to work, but there may be ways around it that I'm 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 bat</a>";
    SESSION::set('recent_attack', $start_of_attack);
    die();
} else {
    SESSION::set('recent_attack', $start_of_attack);
}

If there're ways to improve on that or ways that that is exploitable (beyond the one obvious to me that echoing stuff isn't a good seperation of logic, I'd love to know. Along those lines, munity-wiki-ed.

The background: Ok, I run a legacy BBG over at ninjawars. There is an "attack" that players can make on other players that is initialized via form post. Essentially, we can simplify the situation to pretend that there's a page, lets call it attack.php, with a giant "ATTACK" form post that submits to another php page, lets call it accept_attack.php, and the second page performs the attack functionality, lets say killing other player 1, 2, or 3. The server runs PHP5, Postgresql, Apache

The problems:

  • If I hit that big "ATTACK" button, and it brings me to the accept_attack.php, I can then hit refresh three times, resubmitting each time, to attack again three times in succession.
  • If I open up three tabs of the first page, and hit attack on each page, I end up with three instantaneous attacks that kill players 1, 2, and 3 all at once, and I can just continually refresh to repeat.
  • Despite my attempts to have a "most recent attack" timer that gets saved to the database, players seem to be able to work around it, perhaps just by refreshing three copied tabs in a synchronized enough way, so that they may all retrieve the same timer (e.g. 10:00:00:0000 am) and thus proceed with the resulting processing.

The solution needed:

So how do I prevent the same processing of a certain script from being preformed all at once in triplicate?

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

Edit: Based on the answers, here's what I did to (potentially, before stress testing) solve it: The session answer seemed simplest/most prehensible to implement, so I used that data store. I tested it and it seems to work, but there may be ways around it that I'm 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 bat</a>";
    SESSION::set('recent_attack', $start_of_attack);
    die();
} else {
    SESSION::set('recent_attack', $start_of_attack);
}

If there're ways to improve on that or ways that that is exploitable (beyond the one obvious to me that echoing stuff isn't a good seperation of logic, I'd love to know. Along those lines, munity-wiki-ed.

Share Improve this question edited Jan 19 at 6:51 munity wiki
4 revs, 2 users 100%
Kzqai
Add a ment  | 

5 Answers 5

Reset to default 7

Although womp's Post-Redirect-Get pattern will solve some problems, if they are deliberately gaming the submission process then I doubt it will prevent the problem except against the lazy (as noted in the linked article, submissions prior to the 302 response will be multiple as the redirect hasn't happened yet).

Instead you are probably better putting some information token on the attack page that is not easily reproduced. When you accept the attack, push the attack into a database queue table. Specifically, store the information token sent to the attack page when queuing and check to see if that token has already been used before queuing the attack.

A simple source of tokens would be the results of running a random number generator and putting them into a table. Pull the next number for each attack page load and verify that that number had been distributed recently. You can repopulate the tokens on attack page loads and expire any "unused" tokens based on your policy for how long a page should be available before going "stale".

In this way you generate a finite set of "valid" tokens, you publish those tokens on the attack page (one per page) and you verify that they token hasn't already been used on the attack processing page. To create repeat attacks the player would have to determine what tokens are valid... repeating the same post will fail because the token has been consumed. Use a BigInt and a decent pseudo-random number generator and the search space makes it unlikely to be easy to circumvent. (Note, you will need a transaction around the token validation and updates to ensure success with this method.)

If you have user accounts that require a login, you can generate and store these tokens on the user table (again, using a database transaction wrapped around these steps). Then each user would have a single valid token at a time and multiple submissions would be caught in a similar way.

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

In a nutshell, 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 duplicate form submission.

Similar to Godeke's solutions. Couldn't you generate a token with a hidden field on the "Attack" button form and store that in a session? Then on the accept-attack.php page you would check if the $_POST['token'] == $_SESSION['token'].

so you would have something similar to this on 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
   } 

Another solution, would be to serialize the post data (i like JSON myself) 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 should also add a timestamp to the same table, so that the hashes can get deleted/updated after X hours

sample php pseudo-code:

$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 submission of different POST data on a short period of time, but thats also quite easy to check.

This solution should be impossible to circumvent:

1) Add a 'NextAttackToken CHAR(32)' column to your players table, and give each a player a randomly generated MD5 value.

2) On the attack.php page, add a hidden field 'current_token' with the player's current token.

3) In the accept_attack.php page, 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 the table at a time, and even if there are 100 spammed requests at exactly the same time, the first UPDATE by mysql will change the token and stop the other 99 updates from affecting any rows.

发布评论

评论列表(0)

  1. 暂无评论