Codeby web-security - новый курс от Codeby Security School

Представляем вашему вниманию новый курс от команды The Codeby - "Тестирование Веб-Приложений на проникновение с нуля". Общая теория, подготовка рабочего окружения, пассивный фаззинг и фингерпринт, Активный фаззинг, Уязвимости, Пост-эксплуатация, Инструментальные средства, Social Engeneering и многое другое. Подробнее ...


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

Это небольшой урок, в котором мы будем делать форму обратной связи. После того, как пользователь заполнит необходимые поля, введённые данные будут отправляться на сервер посредством AJAX-запроса. Если данные невалидные (например, отсутствуют обязательные для заполнения поля), то соответствующее сообщение будет показано пользователю.

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

Я знаю, что в Интернете огромное множество готовых решений, в том числе с очень красивыми формами. Более того, значительная часть этого кода была «подсмотрена» у других. Главная цель написания статьи — систематизировать собственные знания о функции отправки почты в PHP-скрипте. Если кому-то моя заметка пригодится и сэкономит время, то я буду очень рад.

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

Кстати, для тестирования отправки почты на локалхосте незаменимой станет почтовая заглушка. Поэтому рекомендую посмотреть статью «Почта на локальном сервере — установка почтовой заглушки».

Я разделил код на два файла: файл с формой и файл обработчик/отправитель письма. Конечно, можно всё собрать в один файл, но для образовательных целей лучше полностью разделить PHP и HTML код для наглядности.

Это наша форма:

<form method="post" action="mails_sender.php" id="feedback" name="feedback" enctype="multipart/form-data">
            <table style="width: 100%">
                <tr>
                    <td style="width: 146px">Ваш e-mail</td>
                    <td><input name="youremail" type="text" style="width: 440px" size="20" /></td></tr>
                <tr>
                    <td style="width: 146px">Тема сообщения</td>
                    <td><input name="subject" type="text" style="width: 440px" size="20" /></td></tr>
                <tr>
                    <td>Опции для выбора:</td>
                    <td>
                        <select name="option" style="width: 440px">
                            <option style='color:#CCCCCC;'>- - Выберите из списка - -</option>
                            <option value="Опция 1">Опция 1</option>
                            <option value="Опция 1">Опция 2</option>
                            <option value="Опция 1">Опция 3</option>
                        </select></td></tr>
                <tr>
                    <td style="width: 146px">Ваше сообщение</td>
                    <td>
                        <textarea name="message" style="width: 440px; height: 130px" rows="1" cols="20"></textarea></td></tr>
                <tr>
                    <td style="width: 146px">Ваше имя</td>
                    <td><input name="name" type="text" style="width: 440px" size="20" value="" /></td></tr>

                <tr>
                    <td style="width: 146px">Вы можете присоединить необходимые файлы</td>
                    <td><input name="fileforsending" type="file" style="width: 440px" /></td></tr>
                <tr>
                    <td style="width: 146px">&nbsp;</td>
                    <td><br />
                        <input name="Reset1" type="reset" value="Очистить" style="width: 97px" />
                        <span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
                            <input name="Submit1" type="submit" value="Отправить" style="width: 157px" />
                            &nbsp; </span>
                    </td></tr>
            </table>
</form>

Одной её уже достаточно, чтобы принять и отправить сообщение на сервер. Я не буду останавливаться на описании полей — т.к. это совсем просто.

В первой строке:

<form method=»post» action=»mails_sender.php» id=»feedback» name=»feedback» enctype=»multipart/form-data»>

mails_sender.php — означает имя файла, которому отправляются данные из формы для обработки.

enctype=»multipart/form-data» — выбран этот метод кодирования данных, поскольку к форме может быть присоединён файл.

id=»feedback» name=»feedback» задано и имя и айди для AJAX.

Данная форма вполне работоспособна и может выполнять свои функции. Использование AJAX позволяет отправлять на сервер запросы без перезагрузки страницы. Это здорово улучшает юзабилити, нравится пользователям, ведь в случае ошибки им не нужно заново вводить все данные — достаточно только исправить ошибку.

Этой строкой мы подключаем JQuery, эта библиотека на порядок облегчает AJAX-запросы, да и другие действия, для которых можно использовать «чистый» JavaScript:

<script src=»//code.jquery.com/jquery-2.1.4.min.js»></script>

Ещё две новых строки:

<div style="color: red;" id="result"></div><br />
<img src="ajax-loader.gif" id="load-indicator" alt="loading" style="position:absolute; z-index:1000; display:none;" />

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

Вторая строка — это гифка «ожидание». При загрузке формы для неё задан стиль невидимости.

Код JavaScript/JQuery довольно объёмный, но больше половина строк в нём — это комментарии. Я не буду переписывать здесь содержимое комментариев, посмотрите сами — на мой взгляд, довольно доступно.

Наш JavaScript/JQuery (form.htm) у нас получился таким:

        <script>
            // этот участок кода нужен только для отображения гифки
            // которая даёт пользователю понять, что что-то происходит
            // и нужно подождать
            $(document).ready(function () {
                $(document).ajaxStart(function () {
                    // найдем элемент с изображением загрузки и уберем невидимость:
                    var imgObj = $("#load-indicator");
                    imgObj.show();
                    // вычислим в какие координаты нужно поместить изображение загрузки,
                    // чтобы оно оказалось в серидине страницы:
                    var centerY = $(window).scrollTop() + ($(window).height() + imgObj.height()) / 2;
                    var centerX = $(window).scrollLeft() + ($(window).width() + imgObj.width()) / 2;
                    imgObj.offset({
                        left: centerX,
                        top: centerY
                    });
                });
                //скрываем изображение после окончания AJAX-запроса
                $(document).ajaxStop(function () {
                    $('#load-indicator').hide();
                });
            });

            // назначаем действие на такое событие как отправка формы
            $('#feedback').submit(function (evtObj) {
                evtObj.preventDefault();
                // Если элемент формы fileforsending содержит значения (т.е. выбран файл для отправки),
                // то вместо AJAX-запроса используем FormData()
                // поскольку файлы через AJAX-запросы не передаются                

                if (document.getElementById("feedback").fileforsending.value !== '') {
                    //показываем гифку ожидания
                    var imgObj = $("#load-indicator");
                    imgObj.show();
                    var centerY = $(window).scrollTop() + ($(window).height() + imgObj.height()) / 2;
                    var centerX = $(window).scrollLeft() + ($(window).width() + imgObj.width()) / 2;
                    imgObj.offset({
                        left: centerX,
                        top: centerY
                    });
                    // этот кусок кода я спёр отсюда http://javascript.ru/forum/jquery/40698-ajax-i-otpravka-fajjlov-s-formy.html
                    var form = document.forms.feedback;
                    var formData = new FormData(form);
                    var xhr = new XMLHttpRequest();
                    xhr.open("POST", "mails_sender.php");
                    xhr.onreadystatechange = function () {
                        if (xhr.readyState == 4) {
                            if (xhr.status == 200) {
                                data = xhr.responseText;
                                $("#result").html('Результат выполнения: ' + data);
                                // Письмо отправлено, сбрасываем данные формы если прошло успешно
                                if (data === 'Отправлено письмо с вложениями.') {
                                    document.getElementById("feedback").reset();
                                }
                                //убираем гифку ожидания
                                $('#load-indicator').hide();
                            }
                        }
                    };
                    xhr.send(formData);
                    // В противном случае (не прикреплён файл для отправки)
                    // Делаем AJAX-запрос для отправки письма
                } else {
                    var form = $(this);
                    $.ajax({
                        // Здесь файл, который обрабатывает полученные от пользователя данные и отправляет почту
                        url: 'mails_sender.php',
                        type: 'POST',
                        data: form.serialize(),
                        // Действия в случае успешной отправки AJAX-запроса (а не письма!)
                        // Здесь data - полученное от mails_sender.php сообщение
                        success: function (data) {
                            if (data === 'Отправлено письмо без вложений.') {
                                $("#result").html('Отправлено письмо без вложений.');
                                // Письмо отправлено, сбрасываем данные формы
                                document.getElementById("feedback").reset();
                                // Следующая строка после успешной отправки сообщения
                                // перенаправляет пользователь на любую страницу/сайт
                                // достаточно раскомментировать её и поменять адрес сайта codeby.net
                                // на ваш собственный
                                //document.location.href = 'https://codeby.net';
                            } else {
                                $("#result").html(data);
                            }
                        },
                        error: function (data) {
                            $("#result").html('Результат выполнения: ' + data);
                        }
                    });
                }
            });
        </script>

Второй файл называется mails_sender.php и содержание у него следующее:


Paranoid - курс от Codeby Security School

Представляем вашему вниманию курс от команды codeby - "Комплекс мер по защите персональных данных, анонимности в интернете и не только" Подробнее ...


<?php

// Адрес, куда отправляем письмо
$to = 'mial@localhost';

// Получаем данные от пользователя
// Все данные обязательно нужно проверять на правильность!
$userEmail = filter_input(INPUT_POST, 'youremail', FILTER_VALIDATE_EMAIL);
$subject = filter_input(INPUT_POST, 'subject', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
$option = filter_input(INPUT_POST, 'option', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
$message = strip_tags(filter_input(INPUT_POST, 'message', FILTER_SANITIZE_MAGIC_QUOTES), '<p><a><b><div>');
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);

// Проверка, что данные не пустые. 
// Нас не интересуют анонимки
if (empty($userEmail)) {
    die('Отсутствует или неверен адрес почты.');
// Нас не интересуют послания с пустым сообщением
} elseif (empty($message)) {
    die('Отсутствует сообщение.');
} elseif (empty($subject)) {
    $subject = '[тема не указана]';
}

$the_file = '';
//Если пользователь выбрал файл для отправки
if (!empty($_FILES['fileforsending']['tmp_name'])) {
    // Закачиваем файл
    $path = $_FILES['fileforsending']['name'];
    if (copy($_FILES['fileforsending']['tmp_name'], $path)) {
        $the_file = $path;
    }
}
// Если есть прикреплённый файл, то заголовки чуть другие.
// Поэтому, в зависимости от того, отправил ли пользователь файл,
// выбираем, что делать дальше
$headers = null;

if (empty($the_file)) {
    // эта часть кода отвечает за отправку сообщений без вложений
    // собираем заголовки
    $headers = array();
    $headers[] = "MIME-Version: 1.0";
    $headers[] = "Content-type: text/html; charset=UTF-8";
    $headers[] = "From: $name <$userEmail>";
    $headers[] = "Bcc: JJ Chong <bcc@domain2.com>";
    $headers[] = "Reply-To: Recipient Name <receiver@domain3.com>";
    $headers[] = "Subject: {$subject}";
    $headers[] = "X-Mailer: PHP/" . phpversion();
    // собираем текст письма
    $allmsg = "<p><b>E-mail:</b> $userEmail</p>
        <p><b>Выбранная опция:</b> $option</p>
            <p><b>Сообщение:</b> $message</p>";
    $allmsg = "<html><head><title>Обратная связь</title><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"></head><body>" . $allmsg . "</body></html>";
    // отправляем
    if (!mail($to, $subject, $allmsg, implode("rn", $headers))) {
        echo 'Письмо не отправлено - что-то не сработало.';
    } else {
        echo 'Отправлено письмо без вложений.';
    }
} else {
    // эта часть кода отвечает за отправку сообщений с вложениями
    // читаем отправляемый файл в строку
    $fp = fopen($the_file, "r");
    if (!$the_file) {
        die("Ошибка отправка письма: Файл $the_file не может быть прочитан.");
    }
    $file = fread($fp, filesize($path));
    fclose($fp);
    // удаляем временный файл
    unlink($path);
    // собираем текст письма
    $allmsg = "<p><b>E-mail:</b> $userEmail</p>
        <p><b>Выбранная опция:</b> $option</p>
            <p><b>Сообщение:</b> $message</p>";
    $allmsg = "<html><head><title>Обратная связь</title><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8"></head><body>" . $allmsg . "</body></html>";
    // генерируем разделитель
    $boundary = "--" . md5(uniqid(time()));
    // собираем заголовки
    $headers = array();
    $headers[] = "MIME-Version: 1.0";
    $headers[] = "From: $name <$userEmail>";
    $headers[] = "Bcc: JJ Chong <bcc@domain2.com>";
    $headers[] = "Reply-To: Recipient Name <receiver@domain3.com>";
    $headers[] = "Subject: {$subject}";
    $headers[] = "X-Mailer: PHP/" . phpversion();
    $headers[] = "Content-Type: multipart/mixed; boundary="$boundary"n";
    // собираем текст письма + приложенынй файл
    $multipart = array();
    $multipart[] = "--$boundary";
    $multipart[] = "Content-Type: text/html; charset=UTF-8";
    $multipart[] = "Content-Transfer-Encoding: Quot-Printedrn";
    $multipart[] = "$allmsgrn";
    $multipart[] = "--$boundary";
    $multipart[] = "Content-Type: application/octet-stream";
    $multipart[] = "Content-Transfer-Encoding: base64";
    $multipart[] = "Content-Disposition: attachment; filename = "" . $path . ""rn";
    $multipart[] = chunk_split(base64_encode($file));
    $multipart[] = "--$boundary";
    // отправляем
    if (!mail($to, $subject, implode("rn", $multipart), implode("rn", $headers))) {
        echo 'Письмо не отправлено - что-то не сработало.';
    } else {
        echo 'Отправлено письмо с вложениями.';
    }
}

В нём также много комментариев и, на мой взгляд, код достаточно простой для понимания.

Разве что можно сделать несколько ремарок по поводу блока:

$userEmail = filter_input(INPUT_POST, 'youremail', FILTER_VALIDATE_EMAIL);
$subject = filter_input(INPUT_POST, 'subject', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
$option = filter_input(INPUT_POST, 'option', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
$message = strip_tags(filter_input(INPUT_POST, 'message', FILTER_SANITIZE_MAGIC_QUOTES), '<p><a><b><div>');
$name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);

Про использование фильтров валидации и очистки данных я рекомендую посмотреть официальную справку от PHP. Фильтр filter_input(INPUT_POST, ‘имя_элемента_формы’, FILTER_VALIDATE_EMAIL) валидирует почтовый адрес. Если адрес электронной почты неверен, то возвращается пустая строка.

Для сообщения мы дополнительно используем фильтр тэгов с помощью функции strip_tags. При этом в кавычках мы перечисляем разрешённые тэги. Если мы хотим, чтобы были отброшены все тэги и все специальные символы, то мы можем использовать filter_input(INPUT_POST, ‘имя_элемента_формы’, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW); в результате чего удаляются все тэги, удаляются и кодируются специальные символы.

Но если мы хотим, чтобы некоторые тэги передавались (актуально для HTML-писем с разметкой), то мы используем более мягкий фильтр FILTER_SANITIZE_MAGIC_QUOTES, который эквивалентен функции addslashes(). Применением этого фильтра мы защищаемся от SQL-инжектов. Мы фильтруем остальные тэги для защиты от XSS-атак.

Предотвращения спама из формы обратной связи

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

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

Форма обратной связи может стать объектом «шутки» когда через неё присылается большое количество сообщений (иногда бессмысленных). Нормальные люди при этом не используют форму для отправки (нормальным людям это просто лень), а пишут скрипт/программу, которая напрямую отправляет обработчику сообщения. В качестве простого фильтра можно использовать отсев запросов, в заголовках которых отсутствует должный реферер или присутствует нестандартный пользовательский клиент. К сожалению, эти данные легко подменить, поэтому дополнительно для предотвращения этого, можно в форму добавить скрытое поле. На сервере, при получении сообщения, проверять, присутствует ли значение этого скрытого поля. Если нет — сообщение можно смело игнорировать и не беспокоить им админа.

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

Токен (метку) скрытого поля можно устанавливать и одноразовые. Главное, не делайте слишком короткое время «протухания» токена. Многие валидные пользователи пишут сообщение долго и если пользователь старался, два часа писал умное длинное сообщение, а оно не было принято сервером из-за каких-то фобий админа, то после этого пользователь может быстро написать короткое и обидное сообщение. 🙂

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

Если у вас есть своё видение по этому вопросу (а вариантов здесь множество), то буду рад увидеть ваше мнение в комментариях.


Codeby Market от Сodeby

Мы запустили свой магазин CodebyMarket Equipment for InfoSec. Уже добавили RaspberryAlfa Long-RangeOrange PiArduino и многое другое. Купить Pentesting Devices