views:

138

answers:

5

The background: Ok, I run a legacy BBG over at ninjawars.net. 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 comprehensible 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 combat</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, community-wiki-ed.

+4  A: 

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.

womp
As Godeke noted, PRG will only solve one of your immediate problems, but it's a good first step. If you have a determined enough user base, you'll need more complex solutions. Also, your timer idea has some merit, but it sounds like you need to make it transactional so three requests can't submit a new timer all at once.
womp
Hmmm, that's a concept that I can probably use elsewhere with submitting pages, like my login page, if I can wrap my head around how to actually get the 302 implemented. *smiles*
Tchalvak
+7  A: 

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.

Godeke
I didn't really get the language of this one. I guess it's similar to the concept that I used in the end, but I wasn't really able to decipher it when I was looking to implement.I guess it means something likefirst page wipes tokens from the data store and puts a unique token in.second page checks the unique token and removes all tokens, success or failure.A third, fourth, or fifth page load has no tokens so they all fail.
Tchalvak
There wasn't enough detail given regarding your exact situation, so I spoke very generally about the problem and a possible solution *style*. The basic idea is that you need some token to be generated that can't be guessed easily. One solution would be to attach the token to the user account, but that could be a problem if you allow windows to be open at once, so I suggested that you may need a queue of such tokens. Either way I'm glad you found a solution... I never enable session state (REST is a goal, session defeats that) but it is clearly an acceptable method and worked for you.
Godeke
Ah, that does clear up the point of the queue, and I may find that level of repeatable scale useful/necessary down the line, though I was mainly looking to create a single choke point for a single page at the moment, and since I'm already using Session (which, yes, isn't necessarily ideal) the additional use probably won't make too much difference. Anyway, thanks, maybe I'll be able to/ make use of this concept down the line.
Tchalvak
+1  A: 
jcinacio
+3  A: 
iangraham
this should work, unless someone remembers to view source, and try submitting data with random tokens... how about the reverse? - set the token in session on the 1st page, then validate and delete it from session on the second page.
jcinacio
The reverse should work, and is easy enough to implement.
Jacco
Using SESSION did make it easier to conceive of a solution, and cuts out all that database access cruft, so this is what I took my queue from.
Tchalvak
Err, queue = cue, I mean.
Tchalvak
+1  A: 

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.

too much php