Базовые знания в безопасности сайтов

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

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

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

И еще, я показываю максимально простую реализацию кода. Я знаю про исключения, знаю про 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, например. Это и есть причина всех предостережений о нежелательности скачивания файлов с торрента.

В заключение


Да, разумеется, это далеко не весь спектр атак. Но эти знания уже повысят безопасность ваших проектов
  • +6
  • 6,8k
  • 9
Поделиться публикацией

Комментарии 9

    0
    Сидят, пишут код, а потом в корне сайта появляется backup.sql.gz с логинами-паролям в открытом виде :-)
      0

        0
        Был практический опыт с одним, уже почившим, интернет-магазином от X5, только там было .rar + директория .svn :-)
        +2

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


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

        Лучше не делать экранирование ПЕРЕД добавлением в базу. В базе предпочтительнее хранить исходный текст комментария без каких-либо изменений. А вот при чтении текста комментария из базы и перед выводом на страницу как раз и стоит делать htmlspecialchars. (А ещё лучше просто использовать нормальный шаблонизатор и избавиться от htmlspecialchars совсем, но фреймворки я уже упомянул выше.)


        $username = htmlspecialchars(trim($_POST['login']));
        $password = htmlspecialchars(trim($_POST['password']));

        Делать так перед SQL-запросом нет никакого смысла, это не имеет отношения к XSS, никак не защищает от SQL-инъекций и лишь зазря портит спецсимволы в пароле. А вот PDO защищает.


        setcookie('count_try'

        Злоумышленник тупо почистит куки, и все эти ваши защиты отключатся.

          0

          На счёт брутфорса. Таймаут на основе значений в куках? Им же нельзя доверять, их подменить можно…

            0
            «Базовые знания в безопасности сайтов для новорожденных»

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

            Не лучшая идея. А что, если текст нужен в БД «как есть»? Лучше htmlspecialchars() вызывать при выводе данных.
              0
              Ну или TLS. Вообще TLS по сути является приемником SSL, потому что основан на SSL 3.0. В их работе нет существенных отличий.

              Неужели здесь ключевое слово "Ну". Не было бы отличий, не изобретали бы TLS, а сечас уже TLS-1.3. Вообще SSL уже фактически отошел в Лету. И сегодня все больше сайтов переходят именно на TLS-1.3.


              А чтобы получить https протокол для сайта, нужно получить SSL сертификат.

              Наличие сертификата необходимое условие, но недостаточное.

                +4
                В заключении

                Этот подзаголовок особенно доставил. Кто в заключении? Джон? Разработчик? Автор статьи? :)
                  0
                  Рекламируемый
                  Cloudflare, при чем бесплатно.

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

                Самое читаемое