Pull to refresh

Comments 115

Есть замечания по коду:

  • check_auth() лучше переименовать в is_auth()

  • к типам лучше приводить явно, например через (bool) а не через двойное отрицание !!($_SESSION['user_id'] ?? false)

В чем проблема с двойным отрицанием?)

Проблемы нет. Эти претензии больше к стилистике кода.

Тогда например для функции flash добавить возвращаемое void?)

Проблема в том, что это использование инструмента не по назначению. Вам нужно не само отрицание, а лишь его побочный эффект.

Зато короткая и понятная запись

UFO just landed and posted this here

В первую очередь спасибо за туториал для начинающих, таких вещей сильно не хватает. Весь интернет забит жуткими уроками двадцатилетней давности (не будем показывать пальцем, но как тут не вспомнить Отус, с их эталонным говнокодом. А тут всё более-менее современно, хотя и не без шероховатостей. Но нормальный подход к обработке ошибок, подготовленные выражения и корректное хэширование пароля уже дорогого стоит.


У меня тоже будет несколько мелких замечаний.


  1. Во-первых, я бы не стал приучать новичков к статической магии. Потом всё равно придется отучаться. Тем более что в таком простом примере она и не нужна, область видимости везде одна и та же. Вполне можно обойтись обычной переменной $pdo.
  2. Выводить конфиг за пределы сервера не обязательно, а вот что не помешает — это положить его копию в гит, добавив при этом простой код, чтобы на новом сервере легко можно было запуститься


    if (!file_exists('config.php'))
    {
        $msg = 'Создайте и настройте config.php на основе config.sample.php';
        trigger_error($msg,E_USER_ERROR);
    }

  3. Вместо rowCount() я предпочитаю использовать fetch(). Это конечно вкусовщина, но так, все-таки, чуть более универсально — далеко не все базы данных возвращают эту бессмысленную цыферку.
  4. Если пользователь с таким логином не найден, то обращение к $user['password'] выдаст ошибку. Я бы добавил полученный массив в условие, if ($user && password_verify($_POST['password'], $user['password'])) {
  5. Использование функции flash() для валидации данных представляется мне спорным решением. Все-таки, более традиционным подходом является раздельное информирование об ошибках для каждого поля ввода, а с flash() этого не получится.
  6. Я согласен с тем что кастинг двойным отрицанием выглядит анахронизмом. Учитывая, что при этом все равно происходит неявное преобразование int->bool, то я бы всё-таки делал явное, стандартными средствами.
  7. Обновление таблицы по username представляется мне спорным. Ну то есть понятно, что будет работать, но мне кажется что обращение по первичному ключу должно быть просто на автомате.
  8. Я бы чуть больше внимания уделил разделению логики приложения и логики отображения, пусть даже с использованием пресловутых header.php и footer.php. А то ж новички, как только начнут внедрять, тут же налетят на Headers already sent.
1. Я так понимаю речь о функции pdo(). Тут я с вами полностью согласен, но цель статьи, не научить новичка всё делать правильно, а показать, как правильно делать аутентификацию.

4. Тут я не очень понял. Обращение в данному ключу массива в моём примере идет только в одном месте, и то, только после проверки на существование пользователя.

5. Это не валидация данных. Валидации я вообще не уделил внимание в этой статье. Это просто вспомогательная функция для вывода сообщений.
Её можно расширить, сделать ключ 'flash' массивом, и писать туда сколько угодно сообщений. Я так и хотел сделать. Но в этом простом примере не было возможности продемонстрировать несколько сообщений об ошибках.

6. Двойное отрицание, равно как и именование функции check_auth() – это следствие проф. уклона в моей работе. Первое – я много работаю с джаваскрипт, и там двойное отрицание для приведения к булю не порицается, второе – это из ларки, там аналогичный метод называется Auth::check(). По большому счету – вкусовщина.

8. Про разделение логики и представления я может быть черкану отдельную статью, если меня сильно заденет данная тема на тостере =). Здесь это явно выходит за границы поднятой темы.

В целом все замечания дельные. Новичкам стоит обратить на них внимание.

По 4. да, это я зевнул, извиняюсь.
По 1. всетаки непонятно, без pdo() обучение аутентификацию как раз и будет проще. Но в прочем это мелочи.

А мне вот static понравился. Самая простая реализация сервиса без лапши классического синглтона с getInstance.

Синглтон был "классическим" 20 лет назад. И сейчас уже совсем не в моде. Вся индустрия давно уже ушла от всей этой магии "ресурсов, берущихся ниоткуда" в сторону явного объявления всех зависимостей.

Не понял про зависимости и моду. Экземпляр сервиса в DI все равно синглтон.

Синглтон, но не прибитый гвоздями где-то вглубине кода, а прокинутый, как правило, в конструктор класса.

Разница принципиальная. Классический "синглтон с getInstance" берется из воздуха. Те же global, только в профиль. Глобальная зависимость, связность уровня "прибито гвоздями".
"Синглтон" в DI явно прописан и явно передается в параметрах, можно четко видеть откуда ноги растут, подменить, замокать и так далее.

"Классический "синглтон с getInstance" берется из воздуха." - это какой-то зашквар.

Классический (в смысле реализации) синглтон - это класс который создаётся не через конструктор, а через специальный метод (getInstance).

За счёт этого обеспечивается единственность инстанса класса в процессе выполнения программы (скрипта).

И этот процесс параллелен тому как вы прокидываете зависимости или варите связность со спагетти кодом.

поскольку global не обеспечивает единственности единственности переменной в пределах исполнения кода, то аналогия с синглтона натянута на воображение.

А ещё, помимо того что написал FanatPHP — синглтон может не являться таковым ну вообще. Это лишь указание на то какое поведение требуется в рамках контекста.


В частности, если мы говорим об объекте Connection, который вроде как является синглтоном в рамках какого-то обработчика (контроллера, например) — он может быть вполне себе получаться из фекори, которое зависит от объекта контекста обработки (например реквеста или сессии пользователя): $pool->getConnection($request), что позволяет обрабатывать несколько запросов одновременно изолируя транзакции одного пользователя от другого.


Однако в рамках отдельной "сессии" такие объекты продолжат быть синглтонами.


Так что синглтон в рамках DI-контейнера и синглтон в качестве архитектурного паттерна — разные вещи.


P.S. И это если ещё не упоминать про "ленивые" синглтоны, которые хранятся в WeakMap и могут зависеть от объекта, который пока что лежит в памяти: Например, аутентификация пользователя, которая зависит от объекта сессии, которая зависит от объекта реквеста. Как только реквест исчезает из области видимости (т.е. отправлен респонз) — все зависимости в рамках контекста каскадно удаляются.

Маленькое замечание. В продакшине при добавлении пользователя таблицу стоит блокировать во избежание состояния гонки.
Поток A: SELECT… WHERE username = 'vasya'
Поток B: SELECT… WHERE username = 'vasya'
Поток A: Имя не найдено, INSERT… username = 'vasya'
Поток B: Имя не найдено, INSERT… username = 'vasya'
И либо в потоке B ошибка, либо в таблице два Васи.
Мне кажется, это выходит за рамки статьи.
UFO just landed and posted this here

Вот кстати да, причем можно даже не блокировать, а просто ловить исключение при добавлении

Там никак не будет два Васи. Username уникален. БД не позволит. Будет выброшено исключение.

Ну вот кстати здесь соглашусь. Вероятность такого совпадения стремится к нулю, а в том гипотетическом случае, когда двое Серёж все-таки столкнутся лбами, второму просто выпадет стандартная 501 ошибка, он обновит страницу и получит свою ошибку о том что имя занято.

  1. Backend дает возможность зарегистрироваться с пустым именем и паролем (да, я видел required, это ничего не значит).

  2. Наверное, имеет смысл при хранении приводить имя пользователя к одному регистру.

Вы абсолютно правы. Я совершенно не уделил внимания вопросу валидации данных.
Но по второму пункту я не согласен. Возможно для большинства пользователей не имеет значения написали они «Вася» или «вася». Но эта проблема, я считаю, лежит не в компетенции программиста. Тут всё будет решать ТЗ. Если бизнесу не нужно различать такие мелочи, то нужно добавить приведение к общему регистру, если нет, то нет. Вопрос спорный, и непосредственно к программированию отношения мало имеет.

Ок.

  1. У вас в качестве имени пользователя может быть кусок javascript-а. Это уже "дыра в безопасности".

Ok. Как это помешает пользователю зарегистрироваться, или залогиниться?

Смотрите, скорее всего где-то внутри вашей системы (предположительно) есть админка, куда имеют доступ только администраторы. Предположительно, там есть модуль для просмотра зарегистрированных пользователей. Таким образом я могу украсть сессионные коды администратора просто потому, что администратор решил посмотреть список зарегистрированных пользователей.

Справедливо. Но давайте вспомним о том, что это обучающий материал по аутентификации, а не обо всём сопутствующем. Просто тогда бы текста было бы намного больше.
Пока вы стажер – никто вам не даст пилить админку, или какие-то серьёзные вещи.
А если вы хотя бы миддл, то либо вы используете в проекте какой-то шаблонизатор, который по умолчанию экранирует весь вывод, либо самостоятельно это делаете в каждом месте. В результате ваше имя пользователя, содержащее javascript код просто будет выведено текстом.

Простите, но ваш код в таком виде опасен. Его нельзя публиковать в таком виде. Добавьте хотя-бы strip_tags к имени пользователя. В противном случае можно получить проблемы, просто выполняя SQL запрос в phpMyAdmin.

Почему же strip_tags, а не htmlspecialchars?
И где конкретно – на входе или на выходе?

Если выбирать между этими двумя, я бы использовал strip_tags. Так как результат htmlspecialchars можно случайно скопировать и вставить куда-то, где нет экранирования.

Я переформулирую вопрос. Где вы предлагаете резать теги -- перед сохранением в бд, или перед выводом на страницу.

Спойлер: Я уже ответил на оба варианта ответа.

Я считаю, что нельзя хранить в базе уязвимости. Поэтому перед вставкой в базу.

Вам фанат ниже написал. Это не уязвимость сама по себе.

Если вы поменяете имя пользователя при регистрации, то вам следует не забывать это же делать при логине. Имеем две точки входа, про которые нельзя забывать. Пока две.

А если у вас нормальный, централизованный вывод, например через шаблонизатор, то не имеет значения, что вы сохранили в базе. На выходе всегда будет просто текст, грубо говоря.

Это просто классическая без изысков XSS уязвимость

xss здесь только в одном месте - index.php, там где выводится имя пользователя, после welcome back. Учитывая, что доступ к этой странице имеет только сам пользователь, ценность этой уязвимости стремится к нулю.

Ну кстати вот на это я не обратил внимание и это реальный стыд. И то что не обратил, и то что XSS. Вот при выводе-то надо было экранировать обязательно, это, пожалуй, самый серьёзный косяк этого руководства, который надо обязательно исправить

Ваши ответы всегда внушают мне уважение, но сейчас либо вы во второй раз невнимательны, либо я чего то не понимаю. После авторизации на эту страницу может может попасть только сам пользователь. Как он может навредить другим?

Я не показывал общедоступную страницу /users. Где могли бы выводится имена всех пользователей, и где можно было бы воспользоваться этой уязвимостью.

В любом случае я благодарен пользователю @WFF за поднятую тему. Эти комментарии будут полезны начинающим разработчикам

Это неважно, кто куда попадает. Я выше пользователю WFF как раз и говорил о том, что эти мелочи вообще не должны нас волновать. Должно быть простое правило, которые всегда выполняется. Как в том анекдоте, "Рот есть? значит берет". Выводим? Значит экранируем. Правило простое, как валенок. А от всех этих рассусоливаний "тут защищаем, тут не защищаем", "ну тут в данном уникальном случае мы скорее всего не пострадаем" как раз все уязвимости и появляются.


Ну блин если так рассуждать, можно и хэш пароля не через полейсхолдер, а напрямую в запрос фигачить. И потом на 20 экранов оправдываться, что это безопасно. Я вот реально не понимаю — зачем.

Добавьте хотя-бы strip_tags к имени пользователя. В противном случае можно получить проблемы, просто выполняя SQL запрос в phpMyAdmin.

Извините, но это уже совсем странно. Какая связь между strip_tags и SQL запросами в phpMyAdmin?

Да, перебор, там все экранируется

Ну нет, здесь я не соглашусь. Валидация важна, но не она отвечает за безопасность.
С таким же успехом можно сказать, что в качестве имени может быть кусок SQL. Но это никого не парит, потому что мы правильно работаем с SQL в своем коде.
И точно так же, если мы будем правильно работать в своем коде с HTML, то никакой дыры тоже не будет.
И сессионные коды никто не украдёт, потому что при формировании списка пользователей в виде HTML, этот самый HTML будет экранироваться.

Это мы с вами сейчас помним, что имя пользователя надо экранировать. Но для нового разработчика это не очевидно. Т.е., вообще говоря, проблема возникнет практически гарантированно.

Имя пользователя специально не надо экранировать. И помнить ничего не надо.
Просто экранировать надо вообще всё.


Ещё раз: отсутствие валидации — это не уязвимость.
Уязвимость — это отсутствие экранирования на выходе.
Валидация не должна использоваться для обеспечения безопасности. потому что невозможно придумать валидацию на все случаи жизни.


Поймите, хотя ваши идеи и могут вам казаться очень правильными, но вся индустрия уже много лет как ушла в совсем другом направлении, безопасность гарантируется на выходе, а не на входе.
И пример с SQL это очень хорошо доказывает: точно так же, как "там все экранируется" в SQL, всё должно экранироваться и в HTML.


Добавлю, что я не топлю против валидации. Валидация очень нужна и важна. Сделали? Молодцы. Не сделали? Не страшно — за безопасность отвечают другие механизмы.

Вы мне зачем-то навязываете спор о точке, где мы сделаем все безопасным. Я же вам говорю, что практика хранить в базе вредоносный код означает, что у вас появляется далеко не нулевая вероятность, что:
1. он будет выполнен;
2. вы об этом узнаете слишком поздно.

Ладно, не буду спорить. В веб-разработке слишком много мифов и заблуждений, всех не переубедишь.

А как вы определяете, что код вредоносный?

Здесь, на Хабре, например, можно постить куски кода

Но если экранировать их на выходе, проблемы не будет. И наоборот, если их прогнать через strip_tags на входе, то на выходе получится шняга.

Ещё проще можно сделать через отдельный обработчик и js, вместо всяких форм

Ребята, давайте релевантные комментарии писать. И хорошо бы грамотные. Ну просто xss к затронутой теме не относится, а уж аргументация вообще уровня пре-джуниора

Не нужно ограничивать комментаторов :)
Это всё равно не работает. На комментарий, который кажется нерелевантным, можно просто не отвечать ;)


А xss, хотя и не относится к теме, но тем не менее относится к общему эффекту от статьи. Любое руководство не должно одной рукой лечить, а другой калечить. Точно так же можно было наплевать и на SQL инъекции — ведь тема-то статьи не работа с SQL, а авторизация!
Вся беда плохих руководств как раз в этой отмазке "уровня пре-джуниора": "я совсем про другое писал!". Мне это сто раз в лицо кидали, когда я говорил что код уязвим к SQL инъекциям.


Экранирование вывода должно делаться на уровне условного рефлекса. И в первую очередь у писателя статьи.

Вот именно поэтому комментарии на Хабре особо ценны, и являются обязательными к прочтению.

Не только "к прочтению".
Я вот не понимаю эту позицию, "указать на недочеты вы можете, но я все равно положу на них с прибором".
Для чего пишется этот туториал? Для удовлетворения собственного эго? Чтобы повисел недельку? Нет? чтобы люди приходили и учились? Ну так зачем учить их плохому-то? И главное, в чем причина такой принципиальной позиции, "сдохну, но не исправлю ни буквы в тексте"? По большинству спорных вопросов никто и не настаивает, но реальный косяк надо взять и исправить, а не писать 100500 нелепых оправданий "я не то имел в виду" и "я вообще про другое".

А я думал на Хабре за такие статьи тухлыми помидорками закидают

Если по делу, как то было на фрилансе прилетел CRUD проект базы данных агенства по недвижимости, с подобной авторизацией, было удивительно, что под одним залогиненным пользователям в системе, можно было почистить объявления у всех остальных пользователей.

Вам не повезло.

А если серьёзно, то если прямо сейчас отрыть поисковик и сделать запрос "авторизация на php", то на первой странице выдаче будет инфа 10-летней давности с sqlинъекциями и хеширование md5. Вы правда думаете, что это лучшая информация?

Я не думаю, что то что есть сейчас в поиске, что это лучшая информация.

Скажу больше, даже некоторые, на курсах по PHP за которые беруг деньги, умудряются в 2022 году учить, что пароль надо хранить в md5 + соль.

UFO just landed and posted this here

Наверное так происходит потому, что реализовывать авторизацию никому не нужно?
Одним не нужно потому, что она уже есть в различных cms и фрейморках, другим - потому, что они совсем уж начинающие?

Но допустим я хочу реализовать авторизацию, первый раз увидев пхп.
Подскажите, насколько важно, чтобы у формы у дочерних дивов класс был mb-3? насколько упадет безопасность при использовании mb-4?

И в целом то понятно, что если из статьи весь не имеющий отношения к проверке пароля, то статья по объему превратится в ответ на вопрос на тостере, только без вопроса, но в текущем виде все это похоже на реферат по информатике от девятиклассника из 2009 года, найденный и исправленный девятиклассником в 2022ом (заменой функции проверки пароля с md5()===password на password_verify)

Ну всё-таки, такие рефераты нужны. Потому что на одного девятиклассника, способного написать такой реферат, приходится сто неспособных даже на такое. Кроме password_verify и password_needs_rehash здесь нормальная обработка ошибок и защита от SQL инъекций. Это уже большой шаг вперёд по сравнению с тем, что мы обычно видим в похапе.


Придирка к mb-3 совсем уж левая. Да, в статье много лишнего, но поставить автору в вину использование бутстрапа — это уж совсем из серии "докопаться до столба". На тостере никогда не пишут законченный вариант — только по отдельности, регистрацию там или авторизацию. Полный гайд от и до всё равно нужен.

"Вчера" это реферат, сегодня оно на хабре, "завтра" оно - основной доклад на phpconf2025. Мне вроде бы и чужого места на диске и чужого времени на прочтение данной статьи не жалко, удивило кол-во комментариев. Если бы не это - просто бы прошел мимо.

Придирка к mb-3 совсем уж левая.

Про mb-3 это не про использование бутстапа, а про не имеющий отношения к сути проблемы код и текст.
Если рассматривать эту писанину как статью, то возникает вопрос каждому абзацу - а зачем оно тут?
Зачем тут SQL со структурой базы/таблицы? Она какая-то сверхоптимальная? В ней учтены какие-то возможные ошибки, которые совершают новички?
А может быть доступ к конфигу include config.php какой-то эталон безопасности или сам представленный конфиг как-то сильно влияет на авторизацию (она же аутентификация, ну да не суть)?
На сколько прибавится баллов к безопасности, если обернуть pdo в функцию?
Может быть использование pdo+ps вместо mysqli как-то увеличит чего-то там? Пропадут потенциальные SQL иньекции? А авторизация тут причем?
Зачем тут шаблоны, верстка, бутстрап и прочее и прочее?

Не хватает еще двух абзацев - как зарегить бесплатный хостинг и загрузить файлы по ftp

Но если отпилить все ненужное, то статьи просто по кол-ву символов не получится, вот и долили воды до реферата.

В итоге получается, что это не статья про "аутентификацию на пхп", а готовый продукт - хоть сейчас в zip архив и публиковать в pear. Либо даже почти готовая книга "самоучитель пхп+бутстрап", судя по кол-ву подробно освещенных тем.

Хотя я и согласен по поводу большинства претензий, я не считаю их принципиальными.
А количество комментариев как раз-таки и говорит об актуальности темы (:

Просто поставьте себя на место человека, который только начал изучать язык. Он берет куски кода из найденной статьи и начинает пробовать. И вдруг оказывается, что не хватает какого-то конфига, нужно создать какую-то таблицу... Возникнут затруднения. Именно поэтому так подробно. И именно на такую аудиторию рассчитан материал.

Выше вы совершенно верно подметили, - это и есть ответ на вопрос на тостере, расширенный до формата небольшой статьи.

А я думал, что когда закидывают тухлыми помидорами, то пишут конкретно, что не так ;)


Каким образом эта авторизация подобная и как через неё можно почистить объявления других пользователей?

<?php

if (check_auth()) {
deleteItem($id);
}
?>

Где $id передается через $_GET или $_POST и банальной заменой на другое число будет успешно удалять объявления других пользователей.

Но ведь в коде нет никакого deleteItem($id); Мне кажется, эта претензия притянута за уши. Проверка прав доступа к записи — это авторизация, а не аутентификауция.

И кстати,


а при выполнении ассоциируем с ними ненадёжные данные (ненадёжными данными следует считать всё, что приходит из вне – $_GET, $_POST, $_REQUEST, $_COOKIE).

— это дичайшая ересь. У которой ноги растут как раз из вот этого "я не про это писал". Так что XSS здесь не случайность, а закономерность.


Потому что вместо беспрекословного следования простым правилам безопасности, "Отправляем данные в SQL? Через плейсхолдер. Отправляем данные в HTML? Через экранирование" начинаем учёный совет с ковырянием в носу: "Тээкс, эти данные у нас откуда? "Извне"? Ненадёжненькие, защищаем. А эти откуда? Из базы данных? Ну это ж свои родимые, пихаем как есть (Pwned!)".


Мало того что такие рассуждения сами по себе не имеют смысла — датабазе абсолютно фиолетово, откуда пришли данные — ей важно, чтобы они не поломали запрос. А из какого "источника" они пришли — её вообще никак не интересует, от слова "совсем". Но главное — именно такие глубокомысленные рассуждения об источниках и приводят в итоге к инъекциям. Потому что даже писатели, мнящие себя гуру, не в состоянии внятно сформулировать, какой источник является "опасным", а уж про новичков и говорить-то нечего (этот хоть спросить догадался, а сколько таких не спросило?). И в итоге именно этот, дифференцированный подход к защите, "здесь защищаем, здесь не защищаем, здесь селёдку заворачиваем" — и является причиной большинства уязвимостей.


В общем я уже жалею обо всех плюсах, которые поставил в этой дискуссии — и из-за косяков статьи, но главное — из-за отношения автора к ним.

Спасибо за статью. Я как раз новичок в этом деле и мне интересно всё что вы обсуждали. Я учусь сам и поэтому может быть не прав. Я сделал чистку ввода до отправки и после уже перед вставкой в базу данных. Теперь никакой js код не проникнит, но вот php код пройдёт. Нужно ли очистить ввод от $ или фрагменты php кода добавленые в get и post запросах просто вызовут ошибку и не смогут навредить сайту?

Какую ошибку?


По поводу чистки, здесь уже обсуждали, что к безопасности она не имеет отношения. Для безопасности данные надо не "чистить", а форматировать, и не "до отправки", а перед использованием.


И если под "чисткой ввода до отправки" имеется в виду чистка на клиенте (то есть в браузере), то у меня для вас плохие новости. Потому что любой код на клиенте можно обойти, не говоря уже о том чтобы отправить данные на сервер напрямую, без всяких браузеров.

Спасибо. Актуальные статьи для новичков по типовым практическим задачам - это очень полезная штука.

Комментарии тоже полезны, а также гарантируют публичную валидацию статьи.

Надеюсь, что продолжите писать подобное.

Мне тоже показалось простым, когда решил сделать свою самую первую авторизацию, и решив сделать самый минимум, все же зашел намного дальше. Сейчас тем более пользователи привыкли ко многим мелочам авторизации, без которых можно вызвать их возмущение. Как например:

1) Восстановление пароля (возможно по email или номеру телефона)

2) Опция "запомнить меня" на данном устройстве (через кукисы)

3) Предотвращение брутфорса (капча плюс другие методы), это может вызвать недовольство пользователей только при взломе, в остальном они против.

4) Подтвержение email (или номера телефона)

5) Возможность пользователю выйти, а также выйти на всех устройствах.

2) Вся статья без этой вещи не стоит выеденного яйца.

А чем плох md5 с солью, как, собственно, и без нее? По-моему, во всех современных cms так и хранится. Я понимаю, что они сами по себе легаси, но все же, best pratices что предлагаете?

md5 слишком быстрый и позволяет подбирать не слишком длинные пароли на видеокарте в разумные сроки (особенно хорошо если есть майнинг-ферма под рукой). То же самое про sha1/sha256/sha512. Поэтому для хэширования паролей изобретают специальные алгоритмы pbkdf2/bcrypt/scrypt/argon2: задача их всех — максимально замедлить подбор


и без нее

А это уже совсем глупо, без соли можно построить радужные таблицы и вскрывать пароли в промышленных масштабах


По-моему, во всех современных cms так и хранится.

Ваши знания устарели лет на пятнадцать


best pratices что предлагаете?

Используемый в статье password_hash по умолчанию использует алгоритм bcrypt

Ваши знания устарели лет на пятнадцать

То есть, например, в Wordpress пароли хэшируются не в md5? Утверждать не буду, но по-моему несколько месяцев назад именно он там и был. Я почти уверен, что и сейчас используется, если в последних апдейтах ничего не меняли.

md5 слишком быстрый и позволяет подбирать не слишком длинные пароли на видеокарте в разумные сроки (особенно хорошо если есть майнинг-ферма под рукой).

То есть хороший пароль по-прежнему проблема подобрать даже с md5.

Разве же пароли вида qwerty становятся проблемными для подбора, если используется bcrypt?

Я уже давно не в теме, но раньше успех определялся короткой базой более-менее популярных паролей, а не базой всех возможных паролей, а сейчас реально подбирать сложные пароли на фермах?

Мне кажется, что это по-прежнему проблема, тянуть такие ресурсы на брутфорс бессмысленно для большинства средних проектов, да и успех не гарантирован, а у +/- выросших из легаси есть двухфакторка или вообще логин по айпишнику закрыт, то есть опять же, даже зная пароль, что с ним делать? Где я не прав?

То есть, например, в Wordpress пароли хэшируются не в md5?

Судя по исходникам на гитхабе — не просто md5, а много раз повторяемый в цикле, что тоже замедляет перебор. А ещё комментарий к коду намекает, что md5 используется не от хорошей жизни, так что не думаю, что пример вордпресса стоит принимать во внимание


становятся проблемными для подбора

Если хэш достаточно медленный, то даже словарные пароли перебрать будет несколько более затруднительно (если пароль не из какого-нибудь топ-100 конечно). А несловарные, но всё ещё короткие пароли можно будет считать даже более-менее безопасными и не заставлять пользователя выдумывать сверхдлинное непонятно что


даже зная пароль, что с ним делать?

Пытаться войти с этим или похожим паролем в другие критичные для пользователя сервисы (почту, банки, соцсети), если пользователь оказался достаточно глуп, чтобы использовать одинаковые или похожие пароли на разных сервисах (как я, например)


Вот моя личная история: несколько лет назад хэш моего 13-символьного пароля (несловарный, с разным регистром, цифрами и спецсимволами) утёк с какого-то сайта (не знаю, с какого конкретно, потому что я слишком глуп) и был успешно взломан на сервисе для взлома паролей Hashes.org — с тех пор мой пароль гуляет по интернету в базах словарных паролей. К сожалению, я не знаю, какой алгоритм хэширования использовался, и спросить уже не у кого — сайт hashes.org уже пару лет не работает. Однако это намекает на то, что md5, вероятно, окажется слишком быстрым даже для несловарных 13-символьных паролей

Пытаться войти с этим или похожим паролем в другие критичные для пользователя сервисы (почту, банки, соцсети), если пользователь оказался достаточно глуп, чтобы использовать одинаковые или похожие пароли на разных сервисах (как я, например)

Из этого списка разве что почта может (а может и нет) оказаться без двухфакторной. Да и такой рандомайз — это как иголку в стоге сена искать. Обычно нужно что-то конкретное для таргета, что вообще имеет смысл перебирать, а то выбирать ценное из пула спама, где ничего ценного может и не быть, тянет на еще одну задачу.

В целом я понял, спасибо за комментарии. Хотя и ситуации мне кажутся синтетическими, допускаю, что где-то это может иметь смысл

Xxs позволяет получить сессию любого пользоватетеля который авторизовался на твком кривом сайте как ваш. Поэтому вывод должен быть экранирован. Sql инекции в pdo маловероятны.

Пара мелочей, как дополнение на тему, что можно сделать ещё:

  1. По поводу конфига. Можно сделать чуть интереснее. Вместо хардкода значений в скрипте использовать переменные окружения. А сами значения вынести или в .env файл (под .gitignore) или на веб-сервер или на уровень ОС. Таким образом в коде получится просто getenv('DB_NAME');, getenv('DB_HOST'); и тд. Это чуть сложнее, чем в вашем варианте. С другой стороны код будет выглядеть полноценным, настраивать который в разных окружениях возможно разными способами. На мой взгляд это предпочтительнее, чем проверять или надеяться на существование файла с конфигом.

  2. Если нужно просто проверить существование пользователя (или любой другой сущности) без необходимости получать саму запись, тогда select * будет лишним. Полей может быть сколько угодно в таблице и нет смысла получать их все для простой проверки. Достаточно ограничить выборку полем id или иным легковесным. Или просто указав select 1 from ... where ... Который вернёт только единицу, если запись существует.

Насчет переменных окружения верно, только надо не забывать, что если писать в .env файл, то во-первых, его надо выносить не только из гита, но в первую очередь — из корневой папки веб-сервера, а во-вторых, надо будет подключать к проекту код, который, собственно, умеет читать эти файлы :)
Так что в целом, для данного уровня это будет перебор.

который, собственно, умеет читать эти файлы

Ходят слухи, что можно воспользоваться parse_ini_file, благо формат .env обратно совместим как с форматом ini, так и с .properties ;)

А вы прочитали мой комментарий, не? Там даже цитатой выделено что при желании ничего подключать не надо и дотэнв можно прочитать встроенными средствами.


И вы тут же кидаете ссылку на влукаса, мол, "нет конечно, надо обязательно подключать всё!" =))) Но хотя бы на влукаса, а не на трешовый от симфони. За что спасибо.

Если поместить данные в переменные окружения ОС или веб-сервера, тогда ничего дополнительно даже подключать не нужно. :)

Вместо пароля достаточно отсылать хеш пароля и логина, таким образом вы защитите пароль от компрометации. Или использовать асимметричное шифрование, более сложный и безопасный вариант на клиенте

Береженого Бог бережет..

А чем шифрование SSL не угодило?

Используйте презервативы, они защищают, но не на 100%

Атака на днс, проверка подлинности сертификата, атака посредине компрометация сертификата. Это теоретически, а практически не стоит каких либо затрат

Тут же задам встречный вопрос, насколько вы уверены в центре сертификации его независимости и непредвзятости?

Сказала монашка одевая ...

Скажите, VlasenkoFedor, вы лично применяли когда-либо такой способ? Можно посмотреть на реализацию?

Если ресурс принимает хэш пароля, то, перехватив этот хэш, с ним можно без проблем пройти аутентификацию на этом ресурсе.
Я писал в статье, что нельзя сравнивать хеши. Даже не то чтобы нельзя – просто не имеет смысла.

По поводу password_hash()

Небольшое замечание - по дефолту используется bcrypt, но в мануале на php.net написано что алгоритм может меняться с обновой языка, так что я бы вместо PASSWORD_DEFAULTявно указывал алгоритм чтобы избежать кейса когда ранее зарегестрированный юзер не сможет залогиниться из-за смены алгоритма и хэша как следствие, ну а конкретизировав алгоритм мы можем поле в базе делать на varchar(255) а условно varchar(60)

Для верификации алгоритм указывать не нужно.
Что касается изменения алгоритма или стоимости я писал в статье про функцию password_needs_rehash

Это будет на редкость дурацкая идея. А вот в статье, при всех её недостатках, именно этот момент показан как раз правильно.


Из вашего комментария видно, что вы не совсем понимаете, как работает password_hash() — как сама функция, так и логика хэширования в целом. Давайте разбираться.
Во-первых, судя по всему, вы путаете создание хэша и его проверку. При проверке, вообще-то, алгоритм не указывается. То есть смена дефолтного алгоритма вообще никак не повлияет на проверку. Новые пароли будут создаваться с новым хэшем, а старые будут прекрасно проверяться со старым.
Во-вторых, обратите внимание на функцию password_needs_rehash(), которая используется в коде. Именно она отвечает за перехэширование паролей под новый алгоритм. То есть в том случае, если рекомендованным станет новый алгоритм, то приложение само автоматически перехэширует пароль при логине пользователя.


А если, как вы предлагаете, прибить гвоздями какой-то один алгоритм, то вот тогда-то, в один прекрасный день, после обновления версии РНР, все пароли превратятся в тыкву. Потому что алгоритм, считающийся рекомендованным сейчас, со временем выйдет из употребления. И именно поэтому-то и придумана фишка с рекомендованным алгоритмом. Чтобы при его смене новые пароли сразу хэшировались по-новому, а старые перехэшировались в момент логина.

мы можем поле в базе делать на varchar(255) а условно varchar(60)
А смысл? В обоих случаях поле будет занимать место по длине реально записанной строки + 1 байт на запись длины строки. Ограничивая размер VARCHAR вы не экономите место в базе, а всего лишь не даёте записать в поле строку длиннее, чем указанное ограничение.

В обоих случаях поле будет занимать место по длине реально записанной строки + 1 байт

FIXED ROW FORMAT не согласен с данным утверждением. В нем место в базе будет заниматься именно столько, сколько указано. Минус в том, что строка с меньшим количество символов все равно будет занимать указанный размер. Зато чтение несколько быстрее

В каком смысле быстрее? Вот на основании запроса движок видит, что надо прочитать из файла три блока данных по смещениям ХХХ, УУУ и ЗЗЗ. За счет чего тут чтение будет "несколько быстрее", если у этих блоков будет не разный размер, а одинаковый?


И каким местом varchar относится к FIXED ROW FORMAT?

В каком смысле быстрее? Вот на основании запроса движок видит, что надо прочитать из файла три блока данных по смещениям ХХХ, УУУ и ЗЗЗ.

В том смысле что чтение DYNAMIC ROW подразумевает чтение разных по длине кусков из файла таблицы. И для этого надо знать все смещения каждой строки. И прочитать их отдельно из таблицы смещений, и занять под это кусочек памяти.
В FIXED варианте смещения это простая математика.

varchar относится к FIXED ROW FORMAT

Да не суть важно на самом деле CHAR там или VARCHAR. Храним строку. Понятно, что указание длины VARCHAR это не способ экономии. Хотя я подозреваю, что в каких-то внутренних алгоритмах чтения таблиц с динамическим размером это может учитываться при расчетах смещений и их перестроении

Какая-то очень смешная каша в голове :)
Судя по всему, для вас поиск по БД — это что-то типа работы с линейным списком. Выборка для вас означает исключительно получение записи по её позиции что ли? Потому что "чистая математика" будет только в том случае, если надо получить "третью, пятую и десятую" записи от начала файла. При том что в реальности таких задач, разумеется, не бывает. А бывает задача получить все записи, где в поле имя стоит значение "Вася". И как здесь поможет "математика" — загадка.

И как здесь поможет "математика" — загадка.

Внезапно, все записи с полем "Вася" будут при наличии индекса иметь как раз "третья, пятая и десятая", что приводит к "простой математике". И вся задача сведется к "переходим к N*row_length, читаем row_length", повторяем для всех последующих смещений

Ага, то есть если я правильно понял вашу картину мира, индекс всегда должен хранить цыферку, что для случая динамических строк приводит к необходимости иметь отдельную "таблицу смещений", которая, судя по всему, представляет собой таблицу с вашим любимым фиксированным форматом строк. Из которой по математически вычисленному смещению получаем наконец-то смещение записи в основной таблице. Так?


Я не знаю, может быть в 50-х годах прошлого века и было что-то подобное, но за прошедшие с тех пор 70 лет кому-то в голову пришла простая мысль, что в индексе можно сразу хранить смещение. Получая его без циферок, математики и "отдельной таблицы". То есть поиск записей для таблицы с динамическими строками вообще ничем не отличается от таблицы с фиксированными.

Я вам пр Фому, вы мне про Ярему. (с) народ

Давайте просто посчитаем. Сколько в простейшем варианте надо будет хранить данных о смещениях в "индексе"?
В динамическом варанте надо хранить и смещение и длину строки.
В фиксированном только номер строки, ибо её длина известна исходя из структуры. То есть, даже если учесть, что смещение и номер строки хранятся в однотипной структуре (числа смещений в большой таблице явно могут быть на порядок больше, чем количество строк), то хранение смещений в динамике уже занимает в два раза больше места. А мы всего-то сделали индекс на одной колонке.

с вашим любимым фиксированным форматом строк

С чего вы взяли, что он мой любимый? Может я вообще люблю Aria + Page Row Format. Не вопрос, можно, конечно, порассуждать, что фиксед формат в некоторых случаях полезен, но однозначно не в случае с большим количеством строк разной длины. А вот для хранения хешей одной и той-же длины (да хоть того же md5) или только числовых значений - вполне себе имеет место на существование.

Если брать частный случай MySQL/MariaDB etc и не InnoDB таблицы, то фиксед еще и чинится проще, и менее фрагментирован.

В обоих случаях объем хранимых данных пренебрежимо мал. И от количества индексов не зависит, поскольку смещение хранится только в первичном. Повторюсь, возможно, лет 50 назад разница между 4 и 8 байтами имела настолько большое значение, чтобы упираться фиксированные строки, но мы сейчас в 21-м. И подход с динамическим форматом строк используется повсеместно.


Не говоря уже о том, что мы мы говорили не про хранение данных, а про скорость выборки.


Что он ваш любимый я взял с того, что вы пришли в этот топик с очень странными заявлениями про FIXED ROW FORMAT и "указание длины VARCHAR это не способ экономии"(?!), хотя это всё не имеет никакого отношения к обсуждаемому вопросу: в тексте явно упоминается mysql с движком innodb, в котором форматом строки умолчанию является динамический COMPACT.

Прочитайте внимательно коментарий ниже. Фрагментация данных в динамических форматах в том числе ведет к увеличению времени выборки. Прост потому, что будет храниться не одно смещение.
Первое что нашлось тупо в гугле
https://www.soliantconsulting.com/blog/mysql-optimization-myisam/
Да, там MyISAM, но и fixed у человека показал результаты на 40% быстрее, чем dynamic. Для InnoDB все еще сложнее.

разница между 4 и 8 байтами имела настолько большое значение

Когда у вас будет овер 100кк записей, многое будет иметь значение. И каждый байт, и iops дисков (мы ведь не говорим применительно к топику за суровый хайлоад с безразмерными ресурсами и бесконечным баблом на масштабирование, да?)

О господи, я уж надеялся что вы угомонились. Давайте договоримся, что вы немеряный спец, который пришел обронить скупые крупинки сокровенного знания в наши немытые головы, а мы все сынки перед вами. И на сём сочтём цели вашей высокой миссии успешно достигнутыми.


Я даже не буду придираться к тому, что мы тут лопотали что-то про индексы, а гений из интернетов принципиально их к своей базе не добавлял.

что вы немеряный спец

Да что Вы, я просто нуб, но практик.

Для практика вы выдаёте какие-то совсем оторванные от жизни идеи, например в топике про таблицу пользователей начинаете вещать про колонку фиксированной длины для хранения пароля. Если бы нам нужно было хранить только пароль, то этот глубокомысленный ответ нам бы возможно и пригодился. Но поскольку в таблице хранятся в основном данные переменной длины, то при чем здесь фиксированный формат — загадка.


Для практика вы приводите слишком абстрактные рассуждения про какие-то изолированные воображаемые случаи, и подкрепляете их бегло нагугленными статьями из интернета. Но никогда — собственными примерами из своей практики. Один такой практик тут уже топил за хэширование паролей на клиенте. Когда я его попросил привести пример реализации, он бодро слился.

топил за хэширование паролей на клиенте. Когда я его попросил привести пример реализации, он бодро слился

Раз уж по теме, давайте по теме.

Вообще хеширование пароля на клиенте бывает, у нас в одном из проектов, например, введеный пользователелем пароль таки улетает на сервер в md5. Который внутри в чистом md5 не хранится и не сравнивается. Так исторически сложилось во времена отсутствия доступных SSL сертификатов и работы на HTTP.

Но поскольку в таблице хранятся в основном данные переменной длины, то при чем здесь фиксированный формат — загадка

Если у вас 2 поля переменной длины, а 10 полей нет - стоит задуматься о том, чтоб вынести данные переменной длины в другую таблицу. Тут можем углубиться в тему нормализации БД, но для данного топика это сложно, кмк. Учитывая тему аутентификации - на практике в нашей таблице (фиксированного формата) и логин, и телефон и пароль хранятся в хешированном виде. И конечно, в этой таблице нет другой информации о пользователе. Ибо для аутентификации она в принципе не нужна.

собственными примерами из своей практики

Форум, в котором, на данный момент, 900к+ тем и 35+ миллионов сообщений. И да, там фиксированные таблички, а тексты хранятся отдельно. Достаточно для практики?

и логин, и телефон и пароль хранятся в хешированном виде.

Поясните про хешированные логин и телефон?
Как ими пользоваться потом: как дернуть пользователя, как отправить при необходимости смс на указанный номер?

 как дернуть пользователя, как отправить при необходимости смс на указанный номер?

От задачи зависит, если вам надо отослать на телефон или в почту не по запросу пользователя (ну например уведомление о каком-то событии) - то конечно их придется хранить в открытом или обратимом виде. А если пользователь вводит почту в качестве логина или телефон явно, то всегда можно принять, захешировать так же, как у вас они хранятся и искать уже по хешу.

Достаточно для практики?

Нет, разумеется. Практика, для топящего за какую-то идею, заключается не в том что "я папробовал — ниупало", а в предоставлении практического кейса, когда с помощью рекламируемого решения удалось решить практическую проблему. То есть, в данном случае, "без фиксированного формата были такие и такие проблемы. после введения оного такие и такие проблемы исчезли". Вот это и есть практика. А не ковыряние в носу на абстрактные темы и не "ну вот мы используем, а зачем — не знаем".


Если у вас 2 поля

Ну вот опять это "если". Где практика, Зин? Вот что из вас всё время лезет это "если"? Даже когда явно просишь не употреблять сослагательное наклонение — всё равно наши "практики" неизбежно сбиваются на своё извечное "если бабушка будет дедушкой...".


И дальше уже не могут остановить полёт фантазии и рожают совсем уже шедевры


логин, и телефон и пароль хранятся в хешированном виде.

Чё? Я извиняюсь, а это-то из какого пальца высосано? Можно посмотреть на реализацию? Хэшированном, как я понимаю, в тот самый MD5 без рандомной соли "патамушта исторически сложилось"?

рекламируемого решения

Конкретно вам я ничего не рекламирую. Вы хотели практики. Я рассказываю о конкретном практическом кейсе. Который, блин, работает 10+ лет на одном сервере БД 10-ти летнего возраста. Работает хорошо и быстрее, чем в динамическом формате. Точка.

Если вам так хочется - можете сделать свои тесты, заполнить рандомными данными табличку на овер 100к юзеров и тестить до просветления. Заодно пару десятков раз поменяйте всем юзерам в этой табличке с VARCHAR пароли в строну увеличения длины. И потестите снова. Вам только спасибо скажут.

самый MD5 без рандомной соли "патамушта исторически сложилось"

А читаем мы жопой, да? Я по-моему русским языком написал

Который внутри в чистом md5 не хранится и не сравнивается

Конкретную реализацию я вам не буду показывать. Но вообще у каждого юзера своя рандом соль, обновляемая при каждой смене пароля.

Оппа! Я, честно говоря, не особо надеялся, что вы проглотите эту немудрящую наживку. Тут даже бохатого практического опыта не требуется, только минимальная способность сложить два и два.


В принципе конечно, с лихвой было достаточно одного уже гениального блеяния "от задачи зависит" в комментарии выше. Ну то есть не собираясь ничего посылать ни на емейл, ни на телефон (и не имея физической возможности это сделать) мы всё равно зачем-то их у пользователя запрашиваем и храним, гы-гы.


Но этот маленький гвоздик с рандомной солью тоже пригодится. Поскольку, за неимением возможности использовать емейл и телефон по назначению, их можно было хотя бы теоретически использовать просто для идентификации пользователя (хотя с телефоном это всё равно не работает, в случае телефона на практике при авторизации идет отправка смс, но не будем придираться). И если бы у нас не было этой злосчастной рандомной соли, то можно было бы при логине захэшировать этот емейл и получить существующий в БД идентификатор пользователя. Но вот с рандомной солью это у нас никак не получится. Соль к емейлу пользователь при логине не прикладывает. И останется нам, на нашем проекте со "овер 100к юзеров", искать каждый введенный при логине емейл… перебором. Читать из БД каждую строчку, брать из неё соль, хэшировать с её помощью введенный емейл, и сравнивать с хэшем из БД. Если не совпали — переходить к следующей.


Понятно, что тут тоже можно соскочить, и пояснить специально для тупых, "что для идентификации, мол, используется отдельный логин, ха-ха" (правда, тогда снова возникнет неудобный вопрос, а нафига тогда хранить емейл и тлф, если они вообще никак не используются). Или списать на невнимательность, "я не то имел в виду" (хотя спрошено было конкретно про "логин, и телефон и пароль хранятся в хешированном виде", а не про хэширование на клиенте). Но не будем придираться, "и так неплохо получилось!" (с) анекдот про преферанс.


Спасибо за доставленное удовольствие, давно я так не развлекался!

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

Ощущение, что Вы реально читаете все-таки, извините, жопой.
Я не говорил, что логин или телефон хешированы с солью.
Я говорил о том, что для аутентификации (то есть зайти на сайт) не нужны никакие данные кроме логина и пароля (в качестве логина может быть как e-mail, так и телефон).

Ну то есть не собираясь ничего посылать ни на емейл, ни на телефон (и не имея физической возможности это сделать) мы всё равно зачем-то их у пользователя запрашиваем и храним, гы-гы.

Задачи бывают разные. Хранение телефона или e-mail в открытом виде может быть нужно именно в задаче, когда вы отсылаете пользователю уведомление о каком-то событии (неважно что это, уведомление об ответе на его комментарий или еще что-то), без участия самого пользователя. Ну так храните их в профиле пользователя, если так надо. При чем тут аутентификация?

Мы предпочитаем разделять данные аутентификации и профиль пользователя. У вас может быть совершенно другое решение по этому поводу, которое я могу уважать и на своем не настаивать.

Возьмем другую типичную задачу - сбросить забытый пользователем пароль. Ему в любом случае надо ввести логин (телефон или e-mail), который мы можем принять, получить из него хеш, в котором он у нас хранится, проверить наличие хеша в базе и однозначно идентифицировать. В данном случае нам вообще не нужен его где-то сохраненный в открытом виде телефон или e-mail. Он и так его уже ввел. Ввел телефон - получит SMS с кодом, ввел e-mail - получит e-mail.

Ну и в догонку про фрагментацию в Dynamic Format.

Если вы обновите запись и сунете туда больше данных, нежели там было в момент добавления этой записи - "хвост" этих данных будет записан вообще в другом месте. То есть нам надо хранить уже два "смещения"?

Да не суть важно на самом деле CHAR там или VARCHAR

Мне кажется, что всё-таки немножечко важно. По умолчанию при CHAR будет фиксированный формат, при VARCHAR — динамический. N'est-ce pas?

Sign up to leave a comment.

Articles

Change theme settings