Reputation: 23062
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:
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.
Upvotes: 2
Views: 790
Reputation: 90978
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.
Upvotes: 1
Reputation: 4472
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.
Upvotes: 1
Reputation: 438
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
}
Upvotes: 3
Reputation: 16281
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.
Upvotes: 7
Reputation: 116977
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.
Upvotes: 4