Well, most common cases (or the easiest to implement) CSRF looks something like this:
<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=Fred" />
So, if you assume that you are logged into bank.example.com , you are cookies "alive" and will be sent with a request, so the request will do what the attacker wants, therefore:
Cookies Won't Protect You From CSRF
What can you do:
Send as many requests through POST as you can (without disturbing the user), especially editing, creating and deleting. Itβs easier to hide security in input type='hidden' than in URL.
Check referrer (yes, this little thing bothers you almost every CSRF attack from external sites):
$url = parse_url( isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''); if( isset( $url['host']) && ($url['host'] == $_SERVER['SERVER_NAME'])){
Add temporary security tokens to URLs like this /article/delete/25/axdefm ...
If you spent some time creating good URLs, you will be sent because it just messes them up and it will cause some problems, such as:
- several actions at a time
- request flow for new security token
- how much time should be indicated in the token
Decision
You can create a table for security tokens, for example:
tokens ( id INT, user_id INT, created DATETIME, expires DATETIME,
And when an authorization token is required for any action, you download the last from the database or create a new one, say, you will create a new token only if all available currents are older than 15 minutes:
function getToken( $userId, $eventIdentificator){ // Hope this is self explanatory :) $db->deleteExpiredTokens(); // Try to load token newer than 15 minutes $row = $db->fetchRow( 'SELECT value FROM tokens WHERE created >= ADD_INTERVAL( NOW(), -15 MINUTES) AND user_id = ?', array( $userId)); // createToken will return `value` directly if( !$row){ $row = createNewToken( $userId); } else { $row = $row['value']; } // Real token will be created as sha1( 'abacd' . 'delete_article_8'); return sha1( $row . $eventIdentificator); } echo 'url?token=' . getToken( $userId, 'delete_article_' . $article['id']);
How it will work:
- if you request a security token for the same action within 15 minutes, you will receive the same token
- you will receive a unique token for each action
- if you set the expiration of the token within 4 hours, the token will be active from 3:45 to 4:00.
- if an attacker tries to send you 200,000 token requests in one minute, you will still only have one row in the database
- each user will have a maximum of 16 entries in the table at once
How to check a token?
function checkToken( $userId, $eventIdentificator, $token){ $db->deleteExpiredTokens(); // Just find matching token with brute force $rs = $db->fetch_rowset( 'SELECT value FROM tokens WHERE created >= ADD_INTERVAL( NOW(), -15 MINUTES) AND user_id = ?', array( $userId)); while( $row = $rs->fetch_row){ if( $token == sha1( $row['value'] . $eventIdentificator)){ return true; } } return false; }
If you have not made sure that the action does not happen twice (for example, an editing article, this is great for deletion) just add revision_number or something similar to your $eventIdentificator ).
Try to think about what happens if:
- MANY tokens intruder requests
- user will write an article in a few hours
- If you have a table with delete buttons for hundreds of articles
I would go with the mentioned token system, it feels like a balanced solution between user comfort / implementation complexity and security, comments on ideas and notes are expected :)