Сессии в PHP. Создаем невероятную простую систему регистрации на PHP и MySQL С чего все началось

💖 Нравится? Поделись с друзьями ссылкой

Reg.ru: домены и хостинг

Крупнейший регистратор и хостинг-провайдер в России.

Более 2 миллионов доменных имен на обслуживании.

Продвижение, почта для домена, решения для бизнеса.

Более 700 тыс. клиентов по всему миру уже сделали свой выбор.

*Наведите курсор мыши для приостановки прокрутки.

Назад Вперед

Создание простой системы регистрации пользователей на PHP и MySQL

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

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

В этом уроке, мы создадим очень простую систему регистрации, которая не требует и не хранит паролей вообще! Результат будет легко изменить и добавить к уже существующему PHP-сайту. Хотите выяснить, как это работает? Читайте ниже.



Вот как наша супер простая система будет работать:

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

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

Преимущества такого подхода:

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

Недостатки:

Безопасность аккаунта пользователя. Если кто-то имеет доступ к почте пользователя, он может авторизоваться.
- Email не защищён и может быть перехвачен. Имейте в виду, что этот вопрос актуален и в случае, когда пароль был забыт и его необходимо восстановить, или в любой системе авторизации, которая не использует HTTPS для передачи данных (логин/пароль);
- Пока вы настроите как нужно почтовый сервер, существует шанс, что сообщения со ссылками на авторизацию будут попадать в спам;

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

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

Как пользоваться этой системой

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

Вам нужно скачать исходники, приложенные к уроку
- В архиве найти файл tables.sql Импортируйте его в вашу базу данных используя опцию импорта в phpMyAdmin. Альтернативный способ: открыть этот файл через текстовый редактор, скопировать SQL запрос и выполнить его;
- Открыть includes/main.php и заполнить настройки связи с вашей базой данных (указать пользователя и пароль для связи с базой а также хост и имя базы). В этом же файле, вы также должны указать email, который будет использован в качестве оригинального адреса для сообщений отправляемых системой. Некоторые хосты блокируют исходящие мейлы пока в форме не будет указан настоящий email адрес, который был создан из панели управления хостом, так что укажите реальный адрес;
- Загрузите все файлы index.php , protected.php и папки assets и includes через FTP на ваш хост;
- Добавьте код ниже на каждую PHP-страницу, где нужно отобразить форму авторизации;

Require_once "includes/main.php"; $user = new User(); if(!$user->loggedIn()){ redirect("index.php"); }
- Готово!

Для тех же, кому интересно, как это всё работает - вперёд к чтению ниже!

Первый шаг - написание HTM- кода формы авторизации. Данный код располагается в файле index.php . Этот файл также содержит PHP-код, обрабатывающий данные формы и другие полезные функции системы авторизации. Узнать об этом больше можно в разделе ниже, посвящённом обзору PHP кода.

index.php

Tutorial: Super Simple Registration System With PHP & MySQL Login or Register

Enter your email address above and we will send
you a login link.

Login / Register

В головной секции (между тегами и ) я подключил основные стили (в этом уроке они не разбираются, поэтому вы можете посмотреть их сами. Папка assets/css/style.css). До закрывающего тега я подключил библиотеку jQuery и файл script.js, который мы напишем и разберём чуть ниже.


JavaScript

jQuery отслеживает состояние кнопки "Зарегистрироваться/авторизоваться" с помощью функции e.preventDefault() и отправляет AJAX-запросы. В зависимости от ответа сервера, выводит то или иное сообщение и определяет дальнейшие действия/

assets/js/script.js

$(function(){ var form = $("#login-register"); form.on("submit", function(e){ if(form.is(".loading, .loggedIn")){ return false; } var email = form.find("input").val(), messageHolder = form.find("span"); e.preventDefault(); $.post(this.action, {email: email}, function(m){ if(m.error){ form.addClass("error"); messageHolder.text(m.message); } else{ form.removeClass("error").addClass("loggedIn"); messageHolder.text(m.message); } }); }); $(document).ajaxStart(function(){ form.addClass("loading"); }); $(document).ajaxComplete(function(){ form.removeClass("loading"); }); });

был добавлен в форму для отображения текущего состояния AJAX-запроса (это стало возможным благодаря методам ajaxStart() ) и ajaxComplete() , которые вы сможете найти ближе к концу файла).

Этот класс показывает крутящийся анимированный gif-файл (как бы намекающий нам на то, что запрос обрабатывается), и также выступает как флаг, предотвращающий повторную отправку формы (когда кнопка зарегистрироваться была уже однажды нажата). Класс .loggedIn - это другой флаг, - устанавливается тогда, когда был отправлен email. Этот флаг моментально блокирует любые дальнейшие действия с формой.

Схема базы данных

Наша невероятно простая система регистрации использует 2 MySQL таблицы (SQL-код находится в файле tables.sql ). Первая хранит данные об аккаунтах пользователей. Вторая хранит информацию о количестве попыток входа.


Схема таблицы пользователей.

Система не использует паролей, что видно на схеме. На ней же можно увидеть колонку token с токенами, соседствующую с колонкой token_validity . Токен устанавливается как только пользователь подключается к системе, задаёт свой email для отправки сообщения (чуть подробнее об этом в следующем блоке). Колонка token_validity устанавливает время на 10 минут позже, после которого токен перестаёт быть актуальным.


Схема таблицы, считающая количество попыток авторизации.

В обоих таблицах IP-адрес хранится в обработанном виде, с помощью функции ip2long в поле типа integer.

Теперь мы можем написать немножко PHP-кода. Основной функционал системы возложен на класс User.class.php , который вы можете видеть ниже.

Данный класс активно использует idorm (docs), эти библиотеки являются минимально необходимыми инструментами, для работы с базами данных. Он обрабатывает доступ к базе данных, генерацию токенов и их валидацию. Он представляет собой простой интерфейс, позволяющий легко подключить систему регистрации к вашему сайту, если он использует PHP.

User.class.php

Class User{ // Частный ORM случай private $orm; /** * Найти пользователя по токену. Только валидные токены, приняты к рассмотрению. Токен генерируется только на 10 минут с того момента как был создан * @param string $token. Это искомый токен * @return User. Вернуть значение функции User */ public static function findByToken($token){ // найти токен в базе и убедиться, что установлен корректный временной штамп $result = ORM::for_table("reg_users") ->where("token", $token) ->where_raw("token_validity > NOW()") ->find_one(); if(!$result){ return false; } return new User($result); } /** * Авторизовать или зарегистрировать пользователя * @param string $email. Пользовательский email-адрес * @return User */ public static function loginOrRegister($email){ // Если такой пользователь уже существует, вернуть значение функции User от заданного email-адреса хранимого в базе if(User::exists($email)){ return new User($email); } // В противном случае создать нового пользователя в базе и вернуть значение функции User::create от указанного email return User::create($email); } /** * Создать нового пользователя и сохранить в базу * @param string $email. Пользовательский email-адрес * @return User */ private static function create($email){ // Записать нового пользователя и вернуть результат функции User от этих значений $result = ORM::for_table("reg_users")->create(); $result->email = $email; $result->save(); return new User($result); } /** * Проверить, существует ли такой пользователь в базе и вернуть булево значение переменной * @param string $email. Пользовательский email-адрес * @return boolean */ public static function exists($email){ // Существует ли пользователь в базе? $result = ORM::for_table("reg_users") ->where("email", $email) ->count(); return $result == 1; } /** * Создать новый пользовательский объект * @param экземпляр $param ORM , id, email or 0 * @return User */ public function __construct($param = null){ if($param instanceof ORM){ // ORM проверка пройдена $this->orm = $param; } else if(is_string($param)){ // Проверка на email пройдена $this->orm = ORM::for_table("reg_users") ->where("email", $param) ->find_one(); } else{ $id = 0; if(is_numeric($param)){ // идентификатору пользователя передаётся значение переменной $param $id = $param; } else if(isset($_SESSION["loginid"])){ // В противном случае смотри сессию $id = $_SESSION["loginid"]; } $this->orm = ORM::for_table("reg_users") ->where("id", $id) ->find_one(); } } /** * Сгенерировать новый SHA1 токен авторизации, записывает в базу и возвращает его значение * @return string */ public function generateToken(){ // Сгенерировать токен для авторизованного пользователя и сохранить его в базу $token = sha1($this->email.time().rand(0, 1000000)); // Сохранить токен в базе // И пометить его, что он актуален только в течение 10 следующих минут $this->orm->set("token", $token); $this->orm->set_expr("token_validity", "ADDTIME(NOW(),"0:10")"); $this->orm->save(); return $token; } /** * Авторизовать пользователя * @return void */ public function login(){ // Отметить пользователя, как авторизованного $_SESSION["loginid"] = $this->orm->id; // Обновить значение поля базы last_login $this->orm->set_expr("last_login", "NOW()"); $this->orm->save(); } /** * Уничтожить сессию и разлогинить пользователя * @return void */ public function logout(){ $_SESSION = array(); unset($_SESSION); } /** * Проверка, заходил ли пользователь * @return boolean */ public function loggedIn(){ return isset($this->orm->id) && $_SESSION["loginid"] == $this->orm->id; } /** * Проверка является ли пользователь администратором * @return boolean */ public function isAdmin(){ return $this->rank() == "administrator"; } /** * Найти тип пользователя, может быть либо administrator либо regular * @return string */ public function rank(){ if($this->orm->rank == 1){ return "administrator"; } return "regular"; } /** * Метод позволяющий получить приватную информацию пользователя в *качестве свойств объекта User * @param string $key Имя свойства, получающего доступ * @return mixed */ public function __get($key){ if(isset($this->orm->$key)){ return $this->orm->$key; } return null; } }

Токены генерируются с помощью SHA1 алгоритма и сохраняются в базе данных. Я использую функции времени MySQL, дабы задать 10-минутное ограничение актуальности токена.

Когда токен проходит процедуру валидации, мы напрямую говорим обработчику, что мы рассматриваем только токены, у которых ещё не истёк срок годности, хранимый в столбце token_validity.

Обратите внимание, что я использую волшебный метод __get библиотеки docs в конце файла, чтобы перехватить доступ к свойствам объекта User.

Благодаря этому становится возможным получить доступ к информации, хранящейся в базе, благодаря свойствам $user->email, $user->token и др. В следующем фрагменте кода рассмотрим для примера, как использовать эти классы.


Защищённая страница

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

functions.php

Function send_email($from, $to, $subject, $message){ // Хелпер, отправляющий email $headers = "MIME-Version: 1.0" . "\r\n"; $headers .= "Content-type: text/plain; charset=utf-8" . "\r\n"; $headers .= "From: ".$from . "\r\n"; return mail($to, $subject, $message, $headers); } function get_page_url(){ // Определить URL PHP-файла $url = "http".(empty($_SERVER["HTTPS"])?"":"s")."://".$_SERVER["SERVER_NAME"]; if(isset($_SERVER["REQUEST_URI"]) && $_SERVER["REQUEST_URI"] != ""){ $url.= $_SERVER["REQUEST_URI"]; } else{ $url.= $_SERVER["PATH_INFO"]; } return $url; } function rate_limit($ip, $limit_hour = 20, $limit_10_min = 10){ // Количество попыток входа за последний час по этому IP-адресу $count_hour = ORM::for_table("reg_login_attempt") ->where("ip", sprintf("%u", ip2long($ip))) ->where_raw("ts > SUBTIME(NOW(),"1:00")") ->count(); // Количество попыток входа за последние 10 минут по этому IP-адресу $count_10_min = ORM::for_table("reg_login_attempt") ->where("ip", sprintf("%u", ip2long($ip))) ->where_raw("ts > SUBTIME(NOW(),"0:10")") ->count(); if($count_hour > $limit_hour || $count_10_min > $limit_10_min){ throw new Exception("Too many login attempts!"); } } function rate_limit_tick($ip, $email){ // Создать новую запись в таблице, считающей количество попыток входа $login_attempt = ORM::for_table("reg_login_attempt")->create(); $login_attempt->email = $email; $login_attempt->ip = sprintf("%u", ip2long($ip)); $login_attempt->save(); } function redirect($url){ header("Location: $url"); exit; }

Функции rate_limit и rate_limit_tick следят за количеством попыток авторизации за истёкший период времени с момента первой попытки. Попытка входа записывается в базе в столбец reg_login_attempt. Эти функции вызываются когда происходит обработка и отправка данных формы как вы можете видеть из следующего фрагмента кода.

Код ниже взят из файла index.php и он обрабатывает отправку формы. Он возвращает JSON-ответ, который, в свою очередь, обрабатывается jQuery в файле assets/js/script.js , который мы уже разбирали ранее.

index.php

Try{ if(!empty($_POST) && isset($_SERVER["HTTP_X_REQUESTED_WITH"])){ // Output a JSON header header("Content-type: application/json"); // Является ли этот email-адрес валидным if(!isset($_POST["email"]) || !filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)){ throw new Exception("Please enter a valid email."); } // Проверка. Позволено ли пользователю авторизоваться, не превысил ли он количество допустимых подключений? (файл functions.php для большей информации) rate_limit($_SERVER["REMOTE_ADDR"]); // Записать эту попытку авторизации rate_limit_tick($_SERVER["REMOTE_ADDR"], $_POST["email"]); // Отправить письмо пользователю $message = ""; $email = $_POST["email"]; $subject = "Your Login Link"; if(!User::exists($email)){ $subject = "Thank You For Registering!"; $message = "Thank you for registering at our site!\n\n"; } // Попытка авторизовать или зарегистрировать пользователя $user = User::loginOrRegister($_POST["email"]); $message.= "You can login from this URL:\n"; $message.= get_page_url()."?tkn=".$user->generateToken()."\n\n"; $message.= "The link is going expire automatically after 10 minutes."; $result = send_email($fromEmail, $_POST["email"], $subject, $message); if(!$result){ throw new Exception("There was an error sending your email. Please try again."); } die(json_encode(array("message" => "Thank you! We\"ve sent a link to your inbox. Check your spam folder as well."))); } } catch(Exception $e){ die(json_encode(array("error"=>1, "message" => $e->getMessage()))); }

После успешной авторизации/регистрации код выше отправит пользователю ссылку для авторизации. Токен становится доступным, т.к. он передаётся в качестве переменной в генерируемой ссылке методом $_GET с маркером tkn

index.php

If(isset($_GET["tkn"])){ // Является ли этот токен валидным для авторизации? $user = User::findByToken($_GET["tkn"]); if($user){ // Да, является. Осуществить редирект на защищённую страницу $user->login(); redirect("protected.php"); } // Нет, токен не валидный. Осуществить редирект, на страницу с формой авторизации/регистрации redirect("index.php"); }

$user->login()

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

Похожим образом устроена и обработка функции на выход из системы.

index.php

If(isset($_GET["logout"])){ $user = new User(); if($user->loggedIn()){ $user->logout(); } redirect("index.php"); }

В конце кода, я снова поставил редирект на index.php, таким образом параметр ?logout=1 передаваемый посредством URL не требуется.

Наш файл index.php требует доп. защиты - мы не хотим, чтобы люди, которые когда-либо однажды авторизовались в системе опять видели форму регистрации. Для этих целей, мы используем метод $user->loggedIn() .

index.php

$user = new User(); if($user->loggedIn()){ redirect("protected.php"); }

Наконец-то, вот кусок кода, позволяющий защитить страницы вашего сайта и сделать её доступной только после авторизации.

protected.php

// Чтобы защитить каждую страницу на вашем сайте подключите к ней файл // main.php и создайте новый объект User. Вот как это просто! require_once "includes/main.php"; $user = new User(); if(!$user->loggedIn()){ redirect("index.php"); }

После этой проверки можете быть уверенными, что пользователь был успешно авторизован. Вы также можете получить доступ к хранимой информации в базе с помощью свойств объекта $user . Для вывода email-а пользователя и его статуса используйте этот код:

Echo "Your email: ".$user->email; echo "Your rank: ".$user->rank();

Метод rank() используется здесь потому что в базе обычно хранятся номера (0 для обычного пользователя, 1 для администратора) и нам нужно преобразовать эти данные в статусы, к которым они относятся, в чём нам и помогает этот метод.

Чтобы сделать из обычного пользователя администратора, просто отредактируйте пользовательскую запись через phpMyAdmin (или любую другую программу, позволяющую управлять базами данных). Статус администратора не даёт каких-либо привилегий, в данном примере на странице будет выведено, что вы администратор - и всё.

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

Мы закончили!

С этой невероятно супер квази простой формой мы закончили! Вы можете использовать её в ваших PHP-сайтах, это достаточно просто. Также вы можете модифицировать её под себя и сделать её такой, как вы хотите.

Материал подготовил Денис Малышок специально для сайта сайт

P.S. Хотите двигаться дальше в освоении PHP и ООП? Обратите внимание на премиум-уроки по различным аспектам сайтостроения, включая программирование на PHP, а также на бесплатный курс по созданию своей CMS-системы на PHP с нуля с использованием ООП:

Понравился материал и хотите отблагодарить?
Просто поделитесь с друзьями и коллегами!


Сегoдня мы рассмотрим эксплуатацию критической 1day-уязвимости в популярной CMS Joomla, которая прогремела на просторах интернета в конце октября. Речь пойдет об уязвимостях с номерами CVE-2016-8869 , CVE-2016-8870 и CVE-2016-9081 . Все три происходят из одного кусочка кода, который пять долгих лет томился в недрах фреймворка в ожидании своего часа, чтобы затем вырваться на свободу и принести с собой хаос, взломанные сайты и слезы ни в чем не повинных пользователей этой Joomla. Лишь самые доблестные и смелые разработчики, чьи глаза красны от света мониторов, а клавиатуры завалены хлебными крошками, смогли бросить вызов разбушевавшейся нечисти и возложить ее голову на алтарь фиксов.

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

6 октября 2016 года Дэмис Пальма (Demis Palma) создал топик на Stack Exchange , в котором поинтересовался: а почему, собственно, в Joomla версии 3.6 существуют два метода регистрации пользователей с одинаковым названием register() ? Первый находится в контроллере UsersControllerRegistration , а второй - в UsersControllerUser . Дэмис хотел узнать, используется ли где-то метод UsersControllerUser::register() , или это лишь эволюционный анахронизм, оставшийся от старой логики. Его беспокоил тот факт, что, даже если этот метод не используется никаким представлением, он может быть вызван при помощи сформированного запроса. На что получил ответ от девелопера под ником itoctopus, подтвердившего: проблема действительно существует. И направил отчет разработчикам Joomla.

Далее события развивались самым стремительным образом. 18 октября разработчики Joomla принимают репорт Дэмиса, который к тому времени набросал PoC, позволяющий регистрировать пользователя. Он опубликовал заметку на своем сайте , где в общих чертах рассказал о найденной проблеме и мыслях по этому поводу. В этот же день выходит новая версия Joomla 3.6.3, которая все еще содержит уязвимый код.

После этого Давиде Тампеллини (Davide Tampellini) раскручивает баг до состояния регистрации не простого пользователя, а администратора. И уже 21 октября команде безопасности Joomla прилетает новый кейс. В нем речь уже идет о повышении привилегий . В этот же день на сайте Joomla появляется анонс о том, что во вторник, 25 октября, будет выпущена очередная версия с порядковым номером 3.6.3, которая исправляет критическую уязвимость в ядре системы.

25 октября Joomla Security Strike Team находит последнюю проблему, которую создает обнаруженный Дэмисом кусок кода. Затем в главную ветку официального репозитория Joomla пушится коммит от 21 октября с неприметным названием Prepare 3.6.4 Stable Release , который фиксит злосчастный баг.

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

27 октября исследователь Гарри Робертс (Harry Roberts) выкладывает в репозиторий Xiphos Research готовый эксплоит , который может загружать PHP-файл на сервер с уязвимой CMS.

Детали

Что ж, с предысторией покончено, переходим к самому интересному - разбору уязвимости. В качестве подопытной версии я установил Joomla 3.6.3, поэтому все номера строк будут актуальны именно для этой версии. А все пути до файлов, которые ты увидишь далее, будут указываться относительно корня установленной CMS.

Благодаря находке Дэмиса Пальмы мы знаем, что есть два метода, которые выполняют регистрацию пользователя в системе. Первый используется CMS и находится в файле /components/com_users/controllers/registration.php:108 . Второй (тот, что нам и нужно будет вызвать), обитает в /components/com_users/controllers/user.php:293 . Посмотрим на него поближе.

286: /** 287: * Method to register a user. 288: * 289: * @return boolean 290: * 291: * @since 1.6 292: */ 293: public function register() 294: { 295: JSession::checkToken("post") or jexit(JText::_("JINVALID_TOKEN")); ... 300: // Get the form data. 301: $data = $this->input->post->get("user", array(), "array"); ... 315: $return = $model->validate($form, $data); 316: 317: // Check for errors. 318: if ($return === false) 319: { ... 345: // Finish the registration. 346: $return = $model->register($data);

Здесь я оставил только интересные строки. Полную версию уязвимого метода можно посмотреть в репозитории Joomla.

Разберемся, что происходит при обычной регистрации пользователя: какие данные отправляются и как они обрабатываются. Если регистрация пользователей включена в настройках, то форму можно найти по адресу http://joomla.local/index.php/component/users/?view=registration .


Легитимный запрос на регистрацию пользователя выглядит как на следующем скриншоте.


За работу с пользователями отвечает компонент com_users . Обрати внимание на параметр task в запросе. Он имеет формат $controller.$method . Посмотрим на структуру файлов.

Имена скриптов в папке controllers соответствуют названиям вызываемых контроллеров. Так как в нашем запросе сейчас $controller = "registration" , то вызовется файл registration.php и его метод register() .

Внимание, вопрос: как передать обработку регистрации в уязвимое место в коде? Ты наверняка уже догадался. Имена уязвимого и настоящего методов совпадают (register), поэтому нам достаточно поменять название вызываемого контроллера. А где у нас находится уязвимый контроллер? Правильно, в файле user.php . Получается $controller = "user" . Собираем все вместе и получаем task = user.register . Теперь запрос на регистрацию обрабатывается нужным нам методом.


Второе, что нам нужно сделать, - это отправить данные в правильном формате. Тут все просто. Легитимный register() ждет от нас массив под названием jform , в котором мы передаем данные для регистрации - имя, логин, пароль, почту (см. скриншот с запросом).

  • /components/com_users/controllers/registration.php: 124: // Get the user data. 125: $requestData = $this->input->post->get("jform", array(), "array");

Наш подопечный получает эти данные из массива с именем user .

  • /components/com_users/controllers/user.php: 301: // Get the form data. 302: $data = $this->input->post->get("user", array(), "array");

Поэтому меняем в запросе имена всех параметров с jfrom на user .

Третий наш шаг - это нахождение валидного токена CSRF, так как без него никакой регистрации не будет.

  • /components/com_users/controllers/user.php: 296: JSession::checkToken("post") or jexit(JText::_("JINVALID_TOKEN"));

Он выглядит как хеш MD5, а взять его можно, например, из формы авторизации на сайте /index.php/component/users/?view=login .


Теперь можно создавать пользователей через нужный метод. Если все получилось, то поздравляю - ты только что проэксплуатировал уязвимость CVE-2016-8870 «отсутствующая проверка разрешений на регистрацию новых пользователей».

Вот как она выглядит в «рабочем» методе register() из контроллера UsersControllerRegistration:

  • /components/com_users/controllers/registration.php: 113: // If registration is disabled - Redirect to login page. 114: if (JComponentHelper::getParams("com_users")->get("allowUserRegistration") == 0) 115: { 116: $this->setRedirect(JRoute::_("index.php?option=com_users&view=login", false)); 117: 118: return false; 119: }

А так в уязвимом:

  • /components/com_users/controllers/user.php:

Ага, никак.

Чтобы понять вторую, гораздо более серьезную проблему, отправим сформированный нами запрос и проследим, как он выполняется на различных участках кода. Вот кусок, который отвечает за проверку отправленных пользователем данных в рабочем методе:

Продолжение доступно только участникам Вариант 1. Присоединись к сообществу «сайт», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score!

Решил написать эту заметку, потому как надоело отвечать 100500 раз одно и то же на ВиО.

Многие начинающие веб-программисты рано или поздно сталкиваются с задачей внедрения в свой сайт человеко-понятных линков (ЧПУ). До внедрения ЧПУ все ссылки имеют вид /myscript.php или даже /myfolder/myfolder2/myscript3.php, что тяжело для запоминания и ещё хуже для SEO. После внедрения ЧПУ линки принимают вид /statiya-o-php или даже на кириллице /статья-о-пхп.

Кстати о SEO. Человекопонятные линки на САМОМ деле придумали не для удобного запоминания, а в основном для повышения индексируемости сайта, потому что совпадение поискового запроса и части URL даёт хорошее преимущество в рейтинге поиска.

Эволюция начинающего PHP-программиста может быть выражена следующей последовательностью шагов:

  • Размещение plain-PHP кода в отдельных файлах и доступ к этим файлам через линки вида /myfolder/myscript.php
  • Понимание, что все скрипты имеют значительную часть общего (например, создание подключения к БД, чтение конфигурации, запуск сессии и проч.) и как следствие создание общей начальной точки «входа», некоторого скрипта, который принимает ВСЕ запросы, а потом выбирает — какой внутренний скрипт подключить. Обычно этот скрипт имеет имя index.php и лежит в корне, вследствие чего все запросы (они же URLы) выглядят так: /index.php?com=myaction&com2=mysubaction
  • Необходимость внедрения роутера и переход к человекопонятным линкам.
  • Замечу, что между пунктами 2 и 3 большинство программистов делают очевидную ошибку. Я не ошибусь, если назову это значением около 95% программистов. Даже большинство известных фреймворков содержат эту ошибку. И заключается она в следующем.

    Вместо того, чтобы реализовывать принципиально новый способ обработки линков, ошибочно делается концепция «заплаток и редиректов» на базе.htaccess, которая заключается в том, чтобы с помощью mod_rewrite создавать множество правил редиректа. Эти строки сравнивают URL с каким-либо регулярным выражением и при совпадении расталкивают выуженные из URL значения по GET-переменным, в дальнейшем вызывая всё тот же index.php.

    #Неправильный метод ЧПУ RewriteEngine On RewriteRule ^\/users\/(.+)$ index.php?module=users&id=$1 #....Ещё куча подобных правил...

    У данной концепции множество недостатков. Один из них — трудность создания правил, большой процент человеческих ошибок при добавлении правил, которые сложно выявить, но они приводят к ошибке сервера 500.

    Другой недостаток в том, что часто правится по сути конфига сервера, что само по себе нонсенс. И если в Apache конфиг можно «пропатчить» с помощью.htaccess, то в популярном nginx такой возможности нет, там всё находится в общем файле конфигурации в системной зоне.

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

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

    Суть заключается в том, что начальный запрос всегда хранится в переменной $_SERVER[‘REQUEST_URI’], то есть его можно считать внутри index.php и разобрать как строку средствами PHP со всеми обработками ошибок, динамическими редиректами и проч и проч.

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

    RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f #Если файл не существует RewriteCond %{REQUEST_FILENAME} !-d #И если папка не существует RewriteRule ^.*$ index.php

    Причём это правило можно разместить как в.htaccess, так и в основном файле конфигурации Apache.

    Для nginx соответствующее правило будет выглядеть вот так:

    Location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php last; } }

    Всё просто.

    Теперь рассмотрим кусок кода PHP в index.php, который анализирует ссылки и принимает решение — какой скрипт запускать.

    /часть1/часть2/часть3

    Первое, что приходит в голову — разбить её с помощью explode(‘/’, $uri) и сделать сложный роутер на основе switch/case, анализирующий каждый кусок запроса. Не делайте этого! Это сложно и в итоге приводит код в ужасный непонимабельный и неконфигурабельный вид!

    Я предлагаю более лаконичный способ. Лучше не описывать словами, а сразу показать код.


    4. Необходимо добавить одну таблицу в ту же базу. В ней будут хранится ip-адреса, которые допустили ошибки при входе. Таким образом мы сможем ограничить доступ тем, кто ошибся больше трёх раз подряд на минут 15. Думаю программам, подбирающим пароли, долго придется возиться.
    Зайдем в phpmyadmin и создадим новую таблицу с 3-мя полями:


    ip - ip-адрес.
    date - дата неудачного входа за последние 15 минут у пользователя с данным ip. col - количество ошибок за последние 15 минут у пользователя с данным ip.
    Отлично! Готово, теперь изменим файл проверки логина и пароля, ведь теперь у нас пароль зашифрован. Открываем testreg.php и удаляем все, что дальше удаления пробелов с логина и пароля. Далее добавляем следующий код:

    //удаляем лишние пробелы
    $login = trim($login);
    $password = trim($password);

    // заменяем новым********************************************
    // подключаемся к базе
    include ("bd.php");// файл bd.php должен быть в той же папке, что и все остальные, если это не так, то просто измените путь
    // минипроверка на подбор паролей
    $ip=getenv("HTTP_X_FORWARDED_FOR");
    if (empty($ip) || $ip=="unknown") { $ip=getenv("REMOTE_ADDR"); }//извлекаем ip
    mysql_query ("DELETE FROM oshibka WHERE UNIX_TIMESTAMP() - UNIX_TIMESTAMP(date) > 900");//удаляем ip-адреса ошибавшихся при входе пользователей через 15 минут.
    $result = mysql_query("SELECT col FROM oshibka WHERE ip="$ip"",$db);// извлекаем из базы количество неудачных попыток входа за последние 15 у пользователя с данным ip
    $myrow = mysql_fetch_array($result);
    if ($myrow["col"] > 2) {
    //если ошибок больше двух, т.е три, то выдаем сообщение.
    exit("Вы набрали логин или пароль неверно 3 раз. Подождите 15 минут до следующей попытки.");
    }
    $password = md5($password);//шифруем пароль
    $password = strrev($password);// для надежности добавим реверс
    $password = $password."b3p6f";
    //можно добавить несколько своих символов по вкусу, например, вписав "b3p6f". Если этот пароль будут взламывать методом подбора у себя на сервере этой же md5,то явно ничего хорошего не выйдет. Но советую ставить другие символы, можно в начале строки или в середине.
    //При этом необходимо увеличить длину поля password в базе. Зашифрованный пароль может получится гораздо большего размера.

    $result = mysql_query("SELECT * FROM users WHERE login="$login" AND password="$password"",$db); //извлекаем из базы все данные о пользователе с введенным логином и паролем
    $myrow = mysql_fetch_array($result);
    if (empty($myrow["id"]))
    {
    //если пользователя с введенным логином и паролем не существует
    //Делаем запись о том, что данный ip не смог войти.
    $select = mysql_query ("SELECT ip FROM oshibka WHERE ip="$ip"");
    $tmp = mysql_fetch_row ($select);
    if ($ip == $tmp) {//проверяем, есть ли пользователь в таблице "oshibka"
    $result52 = mysql_query("SELECT col FROM oshibka WHERE ip="$ip"",$db);
    $myrow52 = mysql_fetch_array($result52);
    $col = $myrow52 + 1;//прибавляем еще одну попытку неудачного входа
    mysql_query ("UPDATE oshibka SET col=$col,date=NOW() WHERE ip="$ip"");
    }
    else {
    mysql_query ("INSERT INTO oshibka (ip,date,col) VALUES ("$ip",NOW(),"1")");
    //если за последние 15 минут ошибок не было, то вставляем новую запись в таблицу "oshibka"
    }

    exit ("Извините, введённый вами логин или пароль неверный.");
    }
    else {
    nbsp; //если пароли совпадают, то запускаем пользователю сессию! Можете его поздравить, он вошел!
    $_SESSION["password"]=$myrow["password"];
    $_SESSION["login"]=$myrow["login"];
    $_SESSION["id"]=$myrow["id"];//эти данные очень часто используются, вот их и будет "носить с собой" вошедший пользователь

    //Далее мы запоминаем данные в куки, для последующего входа.
    //ВНИМАНИЕ!!! ДЕЛАЙТЕ ЭТО НА ВАШЕ УСМОТРЕНИЕ, ТАК КАК ДАННЫЕ ХРАНЯТСЯ В КУКАХ БЕЗ ШИФРОВКИ
    if ($_POST["save"] == 1) {
    //Если пользователь хочет, чтобы его данные сохранились для последующего входа, то сохраняем в куках его браузера
    setcookie("login", $_POST["login"], time()+9999999);
    setcookie("password", $_POST["password"], time()+9999999);
    }}
    echo "";//перенаправляем пользователя на главную страничку, там ему и сообщим об удачном входе
    ?>

    5. Полностью изменим главную страничку. Необходимо на ней вывести аватар пользователя, вывести ссылку на выход из аккаунта и добавить чекбокс для запоминания пароля при входе.
    Index.php




    Главная страница


    Главная страница



    6. Необходимо сделать возможность выйти из аккаунта пользователям, которые вошли. На главной странице уже была ссылка на выход. Но этого файла пока не существует. Так создадим новый файл exit.php с кодом:

    Ну вот и все! Пользуйтесь на здоровье! Удачи!

    На прошлом уроке мы разобрались из каких блоков будет состоять шаблон trip , поэтому можно приступать к работе. Для начала создадим две папки:

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

    ВНИМАНИЕ: В папке images шаблона не размещается графика контента!

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

    Далее можно приступать к созданию самого главного файла index.php , который будет определять визуальное расположение элементов сайта и сообщать CMS Joomla в какой блок поместить различные компоненты и модули. Файл является комбинацией PHP и HTML.

    Я всегда при написании кода использую только Macromedia Dreamweaver . Отличная программа, настоятельно советую ее новичкам, т.к. если в процессе работы над кодом вы сделали ошибку, программа обязательно подсветит ваш косяк.

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

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

    index.php

    Заголовок файла

    Заголовок файла состоит из нескольких частей. Первая часть кода PHP заголовка предназначена для того, чтобы убедиться, что к файлу не обращаются напрямую, из соображений безопасности.

    < ?php
    defined ("_JEXEC" ) or die ;
    JHtml::_("behavior.framework" , true ) ;
    $app = JFactory::getApplication() ;
    ?>
    < ?php echo "< ?" ; ?> xml version= "1.0" encoding= "< ?php echo $this - > _charset ?> " ?>

    DOCTYPE – это очень важный параметр, на основании которого браузер решает, как ему отображать эту страницу и как интерпретировать CSS.

    < ! DOCTYPE html PUBLIC "- / / W3C/ / DTD XHTML 1.0 Strict/ / EN" "http:/ / www.w3.org/ TR/ xhtml1/ DTD/ xhtml1- strict.dtd" >

    Следующий фрагмент извлекает установленный язык из глобальной конфигурации.

    < html xmlns= "http:/ / www.w3.org/ 1999/ xhtml" xml:lang= "< ?php echo $this - > language; ?> " lang= "< ?php echo $this - > language; ?> " dir = "< ?php echo $this - > direction; ?> " >

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

    < head>
    < jdoc:include type= "head" / >

    Следующие строки в заголовке содержат ссылки на основные CSS стили Joomla.

    < link rel= "stylesheet" href= "< ?php echo $this - > baseurl ?> / templates/ system / css/ system .css" type= "text/ css" / >
    < link rel= "stylesheet" href= "< ?php echo $this - > baseurl ?> / templates/ system / css/ general.css" type= "text/ css" / >

    Чтобы задействовать стили оформления шаблона, делаем ссылку на файл, содержащий каскадные таблицы стилей template.css, который лежит в папке CSS . Не важно, что этот файл пока пустой, главное его подключить, оформлением займемся потом, когда инсталлируем шаблон на Joomla. Так будет проще наблюдать за результатом.

    < link rel= "stylesheet" href= "< ?php echo $this - > baseurl ?> / templates/ < ?php echo $this - > template ?> / css/ template.css" type= "text/ css" / >

    Следующий фрагмент кода позволяет нам сворачивать левую или правую колонки, если в позициях «left» и « right» не расположено ни одного модуля. В случае если свернуты обе колонки, то контент занимает 100% ширины страницы. Если включена только одна колонка, то контент занимает 80%. При двух включенных колонках на контент приходится 60% ширины страницы.

    < ?php
    if ($this - > countModules("left and right" ) = = 0) $contentwidth = "100" ;
    if ($this - > countModules("left or right" ) = = 1) $contentwidth = "80" ;
    if ($this - > countModules("left and right" ) = = 1) $contentwidth = "60" ;
    ?>

    Заголовок закрывается

    < / head>

    < body>

    Блок «page» содержит оформление только страницы сайта, именно она и будет шириной 950рх.

    < div id= "page" >

    Блок "top" находится в самом верху страницы и содержит в себе два блока "logo " и "user1".

    < div id= "top" >

    В боке «logo» мы разместим графический файл логотипа, это будет прописано в таблицах стилей. А вот автоматический вывод названия сайта прописываем в файле index.php, причем название помещаем в тег H1, что очень важно для поисковой оптимизации.

    < div id= "logo" >
    < h1> < ?php echo $app - > getCfg("sitename" ) ; ?> < / h1>
    < / div>

    Определим позицию «user1» в одноименном блоке для вывода модуля поиска по сайту.

    < div id= "user1" >
    < jdoc:include type= "modules" name= "user1" style= "xhtml" / >
    < / div>
    < / div> < ! - - конец блока top - - >

    Вывод модуля горизонтального меню в блоке «user2» в позиции «user2» . Блок будет сворачиваться, если в этой позиции не будет модуля.

    < ?php if ($this - > countModules("user2" ) ) : ?>
    < div id= "user2 " >
    < jdoc:include type= "modules" name= "user2" style= "xhtml" / >
    < / div>
    < ?php endif ; ?>

    Дальше идет блок шапки сайта «header» . В нем мы определим позицию «header» для вывода модулей. Блок будет сворачиваться, если в этой позиции не будет модуля. Я намеренно расширила возможности этого блока, чтобы иметь возможность разместить в нем не только картинку шапки, но и ротаторы изображений.

    < ?php if ($this - > countModules("header " ) ) : ?>
    < div id= "header " >
    < jdoc:include type= "modules" name= "header " style= "xhtml" / >
    < / div>
    < ?php endif ; ?>

    В блоке «user3» определим позицию «user3» для вывода модулей.

    Блок будет сворачиваться, если в этой позиции «user3» не будет выводится модуль.

    < ?php if ($this - > countModules("user3" ) ) : ?>
    < div id= "user3" >
    < jdoc:include type= "modules" name= "user3" style= "xhtml" / >
    < / div>
    < ?php endif ; ?>

    Открывается блок левой колонки, которая будет сворачиваться, если в позиции «left» не будет ни одного модуля.

    < ?php if ($this - > countModules("left" ) ) : ?>
    < div id= "left" >
    < jdoc:include type= "modules" name= "left" style= "xhtml" / >
    < / div>
    < ?php endif ; ?>

    Открывается самый важный блок контента, который может занимать 100% ширины страницы, 80% и 60%, в зависимости от количества включенных колонок.

    < div id= "content< ?php echo $contentwidth ; ?> " >

    Вывод сообщений в компонентах

    < jdoc:include type= "message" / >

    Вывод содержимого контента.

    < jdoc:include type= "component" style= "xhtml" / >
    < / div> < ! - - конец блока контента- - >

    Открывается блок правой колонки, которая будет сворачиваться, если в позиции «rigth» не будет ни одного модуля.

    < ?php if ($this - > countModules("right" ) ) : ?>
    < div id= "rigth" >
    < jdoc:include type= "modules" name= "right" style= "xhtml" / >
    < / div>
    < ?php endif ; ?>

    Вывод блока «footer» , предназначенного для вывода модуля «HTML код» с информацией об авторских правах. Можно также разместить здесь нижнее горизонтальное меню или модуль представления контента. Блок будет сворачиваться, если в этой позиции «footer» не будет выводится не один модуль

    < ?php if ($this - > countModules("footer" ) ) : ?>
    < div id= "footer" >
    < jdoc:include type= "modules" name= "footer" style= "xhtml" / >
    < / div>
    < ?php endif ; ?>

    Закрываются блок страницы сайта «page», body и весь код.

    < / div> < ! - - конец блока page- - >
    < / body> < ! - - конец блока body - - >
    < / html> < ! - - конец кода- - >

    Мы создали полноценный файл index.php . Теперь вы знаете, при помощи каких команд, и в какой последовательности выводятся блоки шаблона.

    ВНИМАНИЕ: Для того, чтобы код шаблона читался из админпанели joomla, то файл index.php необходимо открыть в редакторе AkelPad и сохранить в кодировке UTF-8, при этом снять галочку BOM. Если вы использовали для работы с файлом программу Macromedia Dreamweaver , то необходимо в вернем меню выбрать пункт "Изменить" > "Свойства страницы" и выбрать кодировку документа Юникод (utf-8), при этом снять галочку "включить сигнатуры юникода (ВОМ)". Однако настоятельно не советую вам редактировать код из админки Joomla, если, что-то накосячите - обратной дороги нет, в отличии от программы Macromedia Dreamweaver , где всегда можно отменить сделанные изменения.

    Само оформление блоков будет описано в template.css. Но настраивать таблицы стилей мы будем после инсталляции шаблона на Joomla 3 (joomla 2.5), а для этого необходимо создать



    Рассказать друзьям