Обнаружена уязвимость функционала «remember me» в Laravel


    Некоторое время назад был опубликован пост с подробным описанием уязвимости функционала «remember me» в фреймворке Laravel. Она позволяла имперсонировать любого пользователя путем создания поддельного кука логина. Разработчики тогда сказали что дыру закрыли. Но более подробный взгляд показывает что гидре отрубали лишь одну голову. Даже если вы не используете данный фреймворк вам все равно будет полезно узнать как не надо имплементировать такой функционал у себя на сайте.



    Главной проблемой является то что токен авторизации в Laravel являет собой просто зашифрованный id пользователя. Почему это плохо? Во-первых если украсть или подобрать ключ используемый для шифровки можно залогиниться под любым пользователем, при этом не придется подбирать его пароль. Конечно получить такой ключ достаточно трудно, но вот например недавно обнаруженная уязвимость OpenSSL Hartbleed позволила бы это сделать. Но это не главная проблема.

    Что будет если кто-то украдет ваш куки?

    Фактически ваш аккаунт уже украден навсегда и вы ничего не можете с этим сделать. Поскольку это просто ваш id, то такой куки нет срока годности, с ним можно будет зайти даже через год и даже если вы поменяете пароль. Это открывает захватчику еще одну дверь: если он однажды подобрал ваш пароль, залогинился а затем сохранил куки себе, то смена пароля вам опять таки уже не поможет. Давайте добавим сюда то, что перехватить ваши пакеты когда вы пользуетесь открытым WIFI ( например в Макдональдс ) очень и очень просто.

    Если не ID, то что?

    Есть много вариантов как сделать remember_me безопасно, все они нуждаются в каком-то уникальном рандомном токене, например:

    1) Создаем табличку в которой сохраняем: user_id, token, expires_on, ip
    2) При логине с включенным «remember me» создаем новый случайный токен, записываем IP пользователя а также задаем срок годности токена.
    3) Когда пользователь приходит с токеном, мы проверяем его валидность по табличке
    4) Логиним пользователя
    5) Удаляем старый токен
    6) Создаем новый токен как в пункте 2 и отдаем его пользователю

    Поскольку токен полностью рандомный подобрать его фактически невозможно. Украсть его намного труднее, так как использовать его можно только один раз. Как дополнительный бонус у нас есть привязка к IP пользователя. Кому интересна подробная имплементация советую прочитать пост на stackoverflow в котором подробно описана вся процедура авторизации пользователя. Я навел упрощенный пример здесь для того чтобы в контрасте показать насколько примитивен подход Laravel.

    Мораль: нельзя доверять безопасность своего приложения фреймворку и надеяться что все сдалали за вас. Кстати создатель фреймворка очень плохо воспринял критику своего подхода и продемонстрировал полное непонимание подхода с токенами.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 20

      +3
      Поставил минус, так как описаны не все действия, которые должны производиться с токеном. Например, не сказано, как токен проверять, как эту табличку зачищать, что делать при смене пароля, каковы правила сосуществования нескольких токенов для одного пользователя.

      Ну и при наличии минимальных знаний криптографии от таблички можно отказаться (см. Flask-Login): делать подписанную куку на основе секретного ключа, хеша от «соленого хешированного пароля», который уже есть в базе, IP и даты протухания. Грубо говоря:

      hash(salted_password_hash) + date + ip, hmac(secret_key, hash(salted_password_hash) + date + ip)

      При получении такой куки проверять все поля. Если не подходит hmac, неправильный IP, протухшая дата или несоответствующий хеш от «соленого хешированного пароля» — кука невалидна, пользователя по ней не пускаем, стираем. Если валидна — переставляем с новой датой протухания. Для каждого пользователя существует множество кук, по которым его пустило бы. Это не проблема, так как для выставления фальшивой, но валидной куки необходимо взломать hmac или украсть с сайта секретный ключ.

      Очевидно, элемент случайности уже есть в salted_password_hash в виде соли. Зачищать на стороне сервера ничего не надо, так как ничего не хранится, кроме того, что и так должно храниться вечно.

      Также очевидно, что смена пароля (в том числе на тот же самый) сделает куки на других устройствах недействительными.
        +2
        Я навел пример имплементации лишь для контраста. Описания подходов достойно отдельной статьи. Я учел ваш коммент и добавил в посте ссылку на подробное описание различных схем авторизации пользователя в целом. Честно говоря, я думал не писать пример имплементации вообще, так как пост больше о доверии к фреймворкам чем об автентификации.
          –1
          Отказ от хранения токена в базе — не очень хороший вариант. Вы лишаетесь возможности сделать функцию «логаут на всех устройствах». А она полезна, например, при подозрении на угон пароля (ну или просто при смене пароля).

          Вообще, подробная статья о способах реализации функции “remember me” очень бы не помешала.
            +2
            Функция «логаут на всех устройствах» в моем варианте как раз есть. Для этого достаточно поменять пароль (в том числе на тот же самый) — при этом изменится его соленый хеш.

            Другое дело, что я забыл включить в куку и в подписанные данные идентификатор пользователя.
            0
            А какой практический смысл считать производный от пользовательских данных хэш, если можно взять рандом?

            ps: а, понял, вы предлагаете совсем отказаться от хранилища на сервере.
              0
              смена пароля (в том числе на тот же самый)

              Откуда вы знаете пароль пользователя? Или вы при логауте предлагаете, каждый раз менять пользователю пароль?
                0
                При обычном логауте предлагаю просто стирать куку. Это не требует ввода пароля и не затрагивает другие устройства.

                Для использования функциональности «логаут на всех устройствах» пользователь в моей схеме действительно должен ввести пароль. Впрочем, я эту функциональность в явном виде могу и не предоставлять. Достаточно фразы «Если вы подозреваете, что злоумышленники получили доступ к вашей учетной записи, смените пароль».
              +16
              Использовать в качестве токена шифрованный id пользователя в недрах фреймворка — это прямо-таки epic fail :/
                +2
                Привязка к ip опять же не самый лучший вариант. У многих ip меняются раз в сутки, а например с GPRS так вообще ip может меняться по несколько раз в пределах одного сеанса. Спустился в метро, потерял связь. Вышел из метро, предположим сессия истекла, идёт попытка авторизации по токену, а ip уже другой.
                  0
                  Ну как я писал, это всего-лишь пример. Конечно есть плюсы и минусы
                  0
                  Ваш способ не работает в тех случаях, когда нужно авторизовываться на разных устройствах, например на работе и дома, а такая вещь нужна практически всегда. Аннулировать просто так предыдущий токен нельзя, иначе в другом месте разлогинит. Фейсбук например предлагает выйти «из всех устройств», по сути обнулить все токены.
                    0
                    Анулируеться только токен по которому пользователь зашел на сайт, другие остаються. Тоесть спокойно можно сидеть нараз с двух девайсов
                      0
                      Если вы говорите про токен в сессии или куках, то он может быть утерян по разными причинам и узнать — по какому токену заходил пользователь будет невозможно.
                        0
                        Постараюсь описать пример:
                        1) Вы логинитесь с компа, создается токен т1
                        2) Вы логинитесь с телефона, создается токен т2
                        3) Через некоторое время вы зпходите с компа опять, системе передается куки т1. Система вас логинит, стирает т1 с базы, создает и отсылает вам токен т3
                        4) Вы заходите с телефона, ваш токен т2 аналогически заменяется на т4.

                        Итого в системе теперь 2 токена (т3 и т4) и телефон и компьютер залогинены
                          0
                          Если я на компе переустанавливаю браузер или куки были утеряны, стерты, кончилось их время, то токен т1 останется в системе и никак его уже клиент (браузер) передать не сможет. Если этот токен вечный или дан на большой период?
                            0
                            у токенов есть срок годности (колонка expires_on). Перед каждым селектом с нее запускаем: DELETE FROM tokens where expires_on < now()
                    0
                    Есть кстати вот хорошее решение для remember me, немного лучше чем то, что вы предложили в посте: jaspan.com/improved_persistent_login_cookie_best_practice
                      0
                      Уже вышел фикс, а вот и гайд: http://laravel.com/docs/upgrade
                        +2
                        «Фикс» на самом деле приводит к тому что теперь только один девайс может иметь «remember_me» токен. Поскольку он хранится в табличке с пользователями то очевидно что каждый пользователь может иметь только один токен. Я не понимаю почему им так трудно сделать правильно?
                        0
                        Только начал изучать Laravel как кандидата на реализацию веб-сервиса — передумал =-/

                        Only users with full accounts can post comments. Log in, please.