Встала задача закрыть дыры в уже готовом PHP приложении(достаточно большом). После недолгих поисков убедился что единственным правильным решением для закрытия CSRF уязвимости является использование одноразовых токенов. Я поставил токены на все POST запросы, и все вроде-бы супер. Но!
Нигде толком не объясняется что делать, если таки придет запрос без токена. Где-то советуют отправлять «forbidden» где-то вообще 404. Как-бы можно и так. Но как быть с кнопкой F5? Там-то посылается уже устаревший токен.
Поиск например тоже проходит через POST. И если пользователь хочет повторить поиск, он обновляет страницу, и его выкидывает к черту на кулички. С точки зрения пользователя, это не совсем логично.
Так-же с историей браузера.
Eсли есть wizard, и пользователь возвращается на предыдущую страницу через кнопку «Back» браузера. Если его выкинуть на 404 или главную страницу, он потеряет все данные которые вводил.
Это только у меня такие проблемы? Кто-нибудь знает красивое решение?
У вас один токен на всю сессию. Этого не достаточно для полноценной защиты. Можно тогда вообще токены не использовать, а брать SessionID — разницы никакой.
Никакого смысла генерить его каждый раз нет. Как правильно уже отметили, он в куках не хранится, и для csrf атак разницы никакой — один и тот же он в рамках сессии или нет.
2slider — аргументируйте, почему этого не достаточно для полноценной защиты?
А как вы узнали SessionID? Перехватили трафик? Ну так и токен там так же можно будет найти.
Я и не говорю что это одно и тоже. Я имел ввиду, что статичный токен лишь немногим безопаснее чем просто SessionID, и не помогает при перехваченном трафике например.
Например если у вас есть сессия, вы можете запросить форму через GET. И там, в теле HTML будет и токен. Или нет?
Конечно зависит от архитектуры и логики приложения, может у SAP этот токен всегда проверяется, или используются какие-то другие средства.
Но у меня уже готовое приложение, которое давно и активно используется клиентами, и переписывать его заново задачи не стояло.
Ну да, в теле html будет токен. Он же и у вас там есть как я понял. Разница только в том что вы каждый раз новый генерите, а мы нет. С точки зрения CSRF атаки я всёравно не вижу чем это хуже.
весь смысл csrf, если упростить, в том, чтобы нельзя было сабмитнуть форму предварительно ее не запросив.
Смысл защиты в том, что зайдя на сайт Васи Пупкина мой браузер от моего имени с моими куками (в которых и сессия) отправит реквест. и сервер воспримет реквест как от меня.
А вот если есть токен, который НЕ хранится в куках, а выдается только в html например — этого токена атакующий сайт знать не может и запрос не пройдёт
Одноразовый токен конечно тоже не дает 100% но он уменьшает вероятность успешной атаки/ уменьшает спектр применимости. Если есть например какая-то дырка, позволяющая видеть весь входящий трафик на сайт, то все токены там будут уже не валидные. Если грубо, то одноразовые токены устаревают часто гораздо быстрее чем сессия. Но вашу мысль я тоже понял. Если по другому не получится нормально — наврно так и сделаем.
А какие-нибудь идеи, кроме использования многоразовых токенов есть?
2 slider, если у вас перехватывают трафик или сперли куки у пользователя (а куки не привязаны к IP), то никакой из вариантов не спасет, что одноразовый токен, что многоразовый.
При перехваченом траффике вам ничего не поможет. там и логин и пароль засвятятся. И SSL не поможет если man in the middle применить. Давайте тогда еще одноразовые пароли использовать. И пофиг что клиенту будет не удобно (ваш токен который на каждый реквест разный это «прощай юзабилити», открыли ваш сайт в двух табах, на обоих разные токены, какой из них верный?)
To further enhance the security of this proposed design, consider randomizing the CSRF token […] for each request. Implementing this approach results in the generation of per-request tokens as opposed to per-session tokens. Note, however, that this may result in usability concerns.
То-есть никак. Или многоразовый токен, или неудобство пользователя.
Генерировать новый токен на каждый запрос может привести к ошибкам при открытии сайта в нескольких вкладках или при аякс-запросах (которые вызовут генерацию нового токена).
Если злоумышленник перехватывает трафик, CSRF ничем не поможет, так как все данные можно прочесть. Используйте HTTPS для защиты от перехвата трафика.
Токен достаточно иметь один на сессию. При несовпадении токена надо показать сообщение «Произошла ошибка. Пожалуйста, проверьте введенные данные, и отправьте форму еще раз».
Естественно, сообщение надо показать вместе с формой с заполненными данными, а не на пустой стрнаице.
GET-формы, не вносящие изменений в базу данных, вроде формы поиска, не надо защищать.
Посмотрите как это реализовано в фрейворках. Например Symfony, YII и т.п.
Обычно логика следующая: в экшене контроллера с формой проверяем — если POST (значит идет сохранение формы), то проверяем токен. Если токен не совпадает, то просто не сохраняем данные, а отображаем ту же форму но с ошибкой «Неверный токен».
И токены обычно используются в формах при сохранении данных. Для формы поиска не вижу смысла их использовать.
Смысла для поиска конечно нет. Но приложение большое и форм много, поэтому я сделал проверку в BasicController, а там отличить POST поиска от других действий не такая тривиальная задача. Кроме того, даже для «не поиска» редирект по F5 на 404 мне не кажется удачной идеей. Странно что это во всех гайдах по CSRF рекомендуют.
> просто не сохраняем данные
Ну в принципе я почти так и сделал. В BasicController очищаю данные из POST, и показывается пустая форма. Но это как-то не очень красиво. Думал, может есть какое-нибудь _правильное_ решение.
Не обновляйте токен после каждого запроса, выставляйте его вместе с авторизационными куками.
Про F5 — можно переводить header(Location:/) после запроса там, где это уместно, или использовать AJAX с тем же токеном
Клиент в поиске может указать какие-то личные данные, которые не должны попасть к третьему лицу, не зависимо от того, что сними будет дальше, Поэтому они не должны попадать в URL (GET).