Хорошо, большинство общего случая (или проще всего реализовать) CSRF выглядит примерно так:
<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=Fred" />
Так что, если вы предполагаете, что вы вошли в bank.example.com
ваши куки "живы" и будут отправлены с запросом, поэтому запрос будет делать то, что хочет злоумышленник, поэтому:
Файлы cookie не защитят вас от CSRF
Что можетвы делаете:
Посылайте столько запросов через POST
, сколько можете (не мешая пользователю), особенно редактирование, создание и удаление.Проще скрыть безопасность в input type='hidden'
, чем в URL.
Проверить referrer (да, эта небольшая вещь предотвращает вас почти от каждой атаки CSRF со стороны внешних сайтов):
$url = parse_url( isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '');
if( isset( $url['host']) && ($url['host'] == $_SERVER['SERVER_NAME'])){
// Request have a go
}
Добавьте временные токены безопасности к URL-адресам, подобным этому /article/delete/25/axdefm
...
Если вы потратили некоторое время на генерацию хороших URL-адресов, вы будете отправлены, потому что это просто приведетих, и это вызывает некоторые проблемы, такие как:
- несколько действий в то время
- поток запросов на новый токен безопасности
- как долго разрешать токенlive
Solution
Вы можете создать таблицу для токенов безопасности, например, такую:
tokens (
id INT,
user_id INT,
created DATETIME,
expires DATETIME, -- One of those two should be enough
value CHAR(30),
PRIMARY (id),
FOREIGN KEY (user_id) ...
);
И когда для какого-либо действия потребуется авторизационный токен, вы загрузитепоследний из БД или создайте новый, допустим, вы создадите новый токен, только если все доступные токены старше 15 минут:
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']);
Как это будет действовать:
- если вы будете запрашивать токен безопасности для того же действия в течение 15 мильЧерез несколько минут вы получите тот же токен
- , который вы получите уникальный токен для каждого действия
- , если вы установите срок действия токена на 4 часа, токен будет активен с 3:45 до 4: 00
- если злоумышленник попытается отправить вам 200000 запросов токенов за одну минуту, у вас останется только одна строка в базе данных
- , каждый пользователь будет иметь максимум 16 записей в таблице одновременно
Как проверить токен?
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;
}
Если вы не хотите, чтобы действие не повторялось дважды (например, редактирование статьи, это прекрасно работает для удаления), просто добавьтеrevision_number
или что-то похожее на ваше $eventIdentificator
).
Попробуйте подумать, что произойдет, если:
- злоумышленник запросит МНОГИ токенов
- пользователь напишет статьюв течение нескольких часов
- если у вас есть таблица с кнопками удаления для сотен статей
Я бы пошел с упомянутой системой токенов, это похоже на сбалансированное решение между пользователемкомфорт / сложность реализации и безопасность, комментарии с идеями иожидаются заметки:)