Привет, Хабр!

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

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

И еще, я показываю максимально простую реализацию кода. Я знаю про исключения, знаю про ORM, про предусмотренную защиту в фреймворках. Моя цель — показать наглядно, так, чтобы поняли все.

И так, пора заканчивать со вступлением и приступать к практике.

Путь от реализации новичка до сколь-нибудь вменяемого результата


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

XSS


Окей, первый тип атак — XSS. Да, старый добрый XSS, о котором слышал каждый. XSS (Cross Site Scripting) — это тип атак, который нацелен на посетителей сайта. Как это происходит: через поле для ввода злоумышленник пишет вредоносный код, который попадает в базу данных и делает свою работу. Обычно таким способом у пользователей крадут cookie файлы, что позволя��т входить в их аккаунты без пароля и логина.

Мы же реализуем более безобидный пример.

Наш разработчик сделал простую форму для добавления комментариев:

Файл index.php
<?php
    $opt = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ];
    $pdo = new PDO("mysql:host=localhost;dbname=".$db,$user,$pass,$opt);
    $pdo->exec("SET CHARSET utf8");

    $query = $pdo->prepare("SELECT * FROM `comments`");
    $query->execute();
    $comments = $query->fetchAll();    

    if ($_POST) {
        $username = trim($_POST['name']);
        $comment = trim($_POST['comment']);

        $query = $pdo->prepare("INSERT INTO `comments` (`username`,`message`) VALUES ('$username', '$comment')");
        $query->execute();

        if ($query) {
            echo 'Комментарий добавлен!';
            header("Location: index.php");
        } else {
            echo 'Произошла ошибка!';
        }
    }
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>XSS</title>
</head>
<body>
    
    <form method="POST" class="addComment">
        <input type="text" name="name" placeholder="Username">
        <textarea name="comment"></textarea>
        <input type="submit" value="Добавить комментарий">
    </form>
    <div class="h2">Комментарии</div>
    <div class="comments">
        <?php
            if ($comments):
                foreach ($comments as $comment):?>    
                    <div class="comment">
                        <div class="comment_username"><?php echo $comment['username'];?></div>
                        <div сlass="comment_comment"><?php echo $comment['message'];?></div>
                    </div>
                <?php endforeach;?>
            <?php else:?>
                <div class="no_comments">Нет комментариев</div>
            <?php endif;?>
    </div>
</body>
</html>

Код весьма прост и не нуждается в объяснениях.

Есть злоумышленник — Джон. Джону стало скучно и он наткнулся на сайт нашего разработчика.
Джон пишет в форму такое сообщение:

<script>document.body.style.backgroundColor = "#000";</script>

И теперь у всех пользователей сайта черный фон. Джон доволен, а разработчик получил опыт и выговор.

Что вообще произошло?

Джон добавил комментарий с JavaScript кодом. При выводе данных на страницу, текстовый комментарий преобразовывается в html код. Html код, увидев использование тега script, добавил его в разметку, а интерпретатор уже выполнил JavaScript код. То есть Джон просто добавил свой кусок js кода к имеющемуся коду сайта.

Как будем исправлять?

Чтобы исправить это недоразумение, была создана функция htmlspecialcars. Суть ее работы в том, что она заменяет символы типа кавычек и скобок на спец символы. Например, символ "<" будет заменён на соответствующий ему символьный код. С помощью этой функции мы обрабатываем данные из формы и теперь js код Джона уже не может причинить вред нашему сайту. Разумеется, если это единственная форма на сайте.

Изменения в коде будут выглядеть так:

Файл index.php
<?php 
if ($_POST) {
    $username = htmlspecialchars(trim($_POST['name']));
    $comment = htmlspecialchars(trim($_POST['comment']));
///
}


SQL Инъекция


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

Как раз о подготовленных запросах мы и поговорим.

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

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

То есть мы будем продолжать работать все с той же формой для добавления комментариев.
Внесем несколько изменений:

1) Предмодерация комментариев.

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

Для реализации задумки добавим в таблицу с комментариями поле «is_moderate», которое будет принимать два значения — «1»(отображаем комментарий) или «0»(не отображаем). По умолчанию, разумеется, «0».

2) Изменим запрос.

Это нужно для большей наглядности. Пусть запрос для добавления комментариев будет выглядеть так:

"INSERT INTO `comments` SET `username`='$username', `message`='$comment'"

Сейчас код работы формы выглядит следующим образом:

Файл index.php
<?php
    $opt = [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
            ];
    $pdo = new PDO("mysql:host=localhost;dbname=".$db,$user,$pass,$opt);
    $pdo->exec("SET CHARSET utf8");
        
    $query = $pdo->prepare("SELECT * FROM `comments` WHERE `is_moderate`='1'");
    $query->execute();
    $comments = $query->fetchAll();    

    if ($_POST) {
        $username = htmlspecialchars(trim($_POST['name']));
        $comment = htmlspecialchars(trim($_POST['comment']));

        $query = $pdo->prepare("INSERT INTO `comments` SET `username`='$username', `message`='$comment'");
        $query->execute();

        if ($query) {
            echo 'Комментарий добавлен!';
        } else {
            echo 'Произошла ошибка!';
        }
    }
?>


Окей, на сайт заходит Джон и, увидев, что комментарии начали проходить модерацию, решил поиздеваться над разработчиком. Тем более, что XSS атаки форма теперь успешно отражает и возможности повеселиться Джона уже лишили. Он оставляет комментарий такого типа: " LOL', is_moderate ='1 " и обходит модерацию.

Почему?

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

При исполнении запроса с комментарием Джона запрос выглядит следующим образом:

"INSERT INTO `comments` SET `username`='John', `message`='LOL', `is_moderate`='1'"

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

Как исправить?

Метод решения проблемы известен уже достаточно давно — подготовленные запросы. Подготовленные запросы — это запросы, которые проходят специальную обработку перед выполнением. Обработка заключается в экранировании дополнительных кавычек. Должно быть вы слышали о такой функции. В PHP она реализована так: " \' ".

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

PDO предоставляет возможность использования масок и плейсхолдеров для реализации подготовленных запросов.

Тогда наш запрос при использовании масок будет выглядеть следующим образом:

Файл index.php
<?php
    $query = $pdo->prepare("INSERT INTO `comments` SET `username`=:username, `message`=:comment");

    $params = ['username' => $username,'comment' => $comment];
    $query->execute($params);


А при использовании плейсхолдеров так:

Файл index.php
<?php 
    $query = $pdo->prepare("INSERT INTO `comments` SET `username`=?, `message`=?");

    $params = [$username,$comment];
    $query->execute($params);


Теперь атака Джона перестает быть актуальной. По крайней мере для данной формы.

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

CSRF атака


CSRF — Cross-Site Request Forgery. Опасен тем, что о нем мало кто знает. Хотя и сделать это достаточно просто.

Как происходит: злоумышленник с другого сайта подделывает форму и заставляет жертву перейти по этой форме. То есть происходит отправка POST запроса. Таким образом подделывается HTTP запрос и на сайте жертвы производится вредоносное действие.

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

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

Наш разработчик сделал форму, он молодец. Она уже умеет защищаться от XSS, SQL инъекций и держит напор Спама. Она выглядит так:

Файл index.php на сайте разработчика
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CSRF</title>
</head>
<body>
    <form action="action.php" method="POST">
        <input type="text" name="username">
        <textarea name="message"></textarea>
        <input type="submit" value="Добавить комментарий">
    </form>
</body>
</html>


Но Джон не так прост. Он получает код формы(просто из исходного кода сайта в браузере) и добавляет форму себе на сайт.

Файл index.php на сайте Джона
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CSRF</title>
    <style>
        form input[type=submit]{
            padding: 15px;
            font-size: 20px;
            color: #fff;
            background: #f00;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <form action="localhost/note/action.php" method="POST">
        <input type="hidden" name="username" value="lol">
        <input type="hidden" name="message" value="Какой плохой сайт! Фу таким быть!">
        <input type="submit" value="Хочу денег!">
    </form>
</body>
</html>


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

Это и есть CSRF атака. В простейшем варианте, разумеется.

У разработчика опять проблемы…

Как исправить уязвимость?

В один прекрасный момент разработчик нагуглит что такое csrf и логика защиты будет заключаться в следующем: для защиты нужно создать csrf токен(набор букв и цифр) и повесить на форму. Так же нужно закрепить этот же токен за пользователем(например, через сессию). А после, при обработке формы, сравнивать эти токены. Если совпали — можем добавлять комментарий.

Реализуем это:

Файл index.php на сайте разработчика
<?php
    session_start();

    $token = '';
    if (function_exists('mcrypt_create_iv')) {
        $token = bin2hex(mcrypt_create_iv(32, MCRYPT_DEV_URANDOM));
    } else {
        $token = bin2hex(openssl_random_pseudo_bytes(32));
    }

    $_SESSION['token'] = $token;
?>
...
<form action="action.php" method="POST">
    <input type="text" name="username">
    <textarea name="message"></textarea>
    <input type="hidden" name="csrf_token" value="<?php echo $token;?>">
    <input type="submit" value="Добавить комментарий">
</form>


Файл action.php
<?php
    session_start();

    if ($_POST) {
        if ($_SESSION['token'] == $_POST['csrf_token']) {
            echo 'Комментарий добавлен!';
        } else {
            echo 'Ошибка!';
        }
    }


Brute Force и Publick Passwords


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

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

Что разработчик может противопоставить? Например, ограничение на количество попыток авторизации в определенный период времени.

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

Файл index.php
<?php
    $opt = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
    $pdo = new PDO("mysql:host=localhost;dbname=".$db,$user,$pass,$opt);
    $pdo->exec("SET CHARSET utf8");

    if (isset($_POST['autorizе'])) {
        $username = htmlspecialchars(trim($_POST['login']));
        $password = htmlspecialchars(trim($_POST['password']));
        $query = $pdo->prepare("SELECT * FROM `users` WHERE `username`=:username AND `password`=:password");
        $query->execute(['username' => $username,'password' => $password]);
        $find_user = $query->fetchAll();

        if ($find_user) {
            echo 'Пользователь найден!';
        } else {
            echo 'Пользователь не найден!';
        }
    }
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Brute Force и Public Passwords</title>
</head>
<body>
    <form method="POST">
        <input type="text" name="login" placeholder="Login">
        <input type="password" name="password" placeholder="Password">
        <input type="submit" value="Войти" name="autorize">
    </form>
</body>
</html>


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

Для этого мы при попытке авторизации будем добавлять пользователю текущее значение времени в куки. И теперь, при попытке авторизации, будем смотреть, чтобы пользователь мог авторизоваться не чаще чем 1 раз в 5 секунд. При чем, при не правильном вводе пароля или логина, мы будем увеличивать таймаут до следующей попытки на 5 секунд.

Файл index.php
<?php 
    $count_next_minit = $_COOKIE['count_try'] ? $_COOKIE['count_try'] : 1;
    $seconds_to_new_try = 5;
    
    $opt = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC];
    $pdo = new PDO("mysql:host=localhost;dbname=".$db,$user,$pass,$opt);
    $pdo->exec("SET CHARSET utf8");

    if (isset($_POST['autorize'])) {
        if ($_COOKIE['last_try']) {
            if ($_COOKIE['last_try'] < time() - $seconds_to_new_try * $count_next_minit) {
                $username = htmlspecialchars(trim($_POST['login']));
                $password = htmlspecialchars(trim($_POST['password']));
                $query = $pdo->prepare("SELECT * FROM `users` WHERE `username`=:username AND `password`=:password");
                $query->execute(['username'=>$username,'password'=>$password]);
                $find_user = $query->fetchAll();

                setcookie('last_try', time(), time() + 3600);
                if ($_COOKIE['count_try']) {
                    $old_value = (int)$_COOKIE['count_try'];
                    setcookie('count_try', $old_value + 1, time() + 3600);
                } else {
                    setcookie('count_try', 1, time() + 3600);
                }

                if ($find_user) {
                    var_dump('Пользователь найден!');
                } else {
                    var_dump('Пользователь не найден!');
                }
            }else{
                var_dump('Слишком часто вводишь пароль! Следующая попытка через ' . $seconds_to_new_try * $count_next_minit . ' секунд');
            }
        }else{
            setcookie('last_try', time(), time() + 3600);
        }
    }
?>


Backtrace


Backtrace — это способ атаки через выводимые ошибки системы. Это и MySQL, и PHP.

Например, Джон ввел не корректный url и ему вывели ошибку о том, что в базе данных нет записи с таким id(если получение записи происходит через id из адресной строки — site.ru/article?id=12). Существуют даже так называемые «дорки» — определенные шаблоны адресов сайтов, при заходе на которые пользователь видит ошибки. А это открывает для Джона возможность с помощью бота пройтись по этому списку адресов и попробовать найти на вашем сайте данную уязвимость.

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

Это можно сделать с помощью функции error_reporting()

Среди аргументов, которые она принимает, находятся: E_ERROR, E_WARNING, E_PARSE, E_NOTICE, E_ALL. Названия говорят сами за себя.

Например, если использовать error_reporting(E_NOTICE), то будут скрыты все ошибки, кроме ошибок типа Notice(предупреждения, например, о том, что отсутствуют данные в массиве $_POST).
Чтобы отключить вывод всех ошибок(что нам, собственно и нужно), нужно использовать эту функцию следующим образом: error_reporting(0)

Логические ошибки


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

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

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

DDOS


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

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

Защиту от таких атак предоставляют либо сами хостинги, либо специальные сервисы по типу Cloudflare.

Как работает защита: сервис Cloudflare предоставляет вам свои DNS сервера, через которые будет проходить трафик. Там он фильтруется, проходя через алгоритмы, известные лишь владельцам и разработчикам сервиса. А после уже пользователь попадает к вам на сайт. Да, конечно, присутствует работа сервера, генерация страницы и все прочее, но мы сейчас не об этом.
К тому же, говоря о DDOS, нельзя не упомянуть про блокировку IP адресов, которую Cloudflare так же предоставляет.

Да, все это не даст 100% гарантии защиты, однако в разы повысит шансы вашего сайта остаться на плаву в момент атаки.

MITM


Man In The Middle — это тип атаки, когда злоумышленник перехватывает ваши пакеты и подменяет их. Все мы слышали, что данные по сети передаются пакетами. Так вот, при использовании протокола http, данные передаются в обычном, не зашифрованном виде.

Например, вы пишите другу «привет», а он получает «пришли мне деньги, вот кошелек».

Именно для решения этой проблемы и был создан протокол https. При его использовании данные будут зашифровываться и Джон не сможет ничего сделать с полученным трафиком.
А чтобы получить https протокол для сайта, нужно получить SSL сертификат. Ну или TLS. Вообще TLS по сути является приемником SSL, потому что основан на SSL 3.0. В их работе нет существенных отличий.

SSL сертификат предоставляет тот же Cloudflare, при чем бесплатно.

Backdoor


Еще немного теории. Ибо этот тип атаки может быть реализован множеством способов и нужно лишь уловить суть. Backdoor — это тип скрытой атаки, при котором скрипт сам делает что-то в «фоне». Чаще всего это плагины или темы WordPress, скачанные с торрента. Сам плагин/тема будет вполне адекватно работать, но определенный кусок скрипта, дописанный в код плагина/темы, будет в тайне делать что-то. Тот же SPAM, например. Это и есть причина всех предостережений о нежелательности скачивания файлов с торрента.

В заключение


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