Java: Веб-технологии

Теория: Безопасность

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

Безопасность веб-приложений — важная тема, которой мы уделим весь этот урок.

Главное правило безопасности

Оно звучит так:

Никогда не доверяйте пользователям

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

package org.example.hexlet;

import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
        });

        app.get("/users/{id}", ctx -> {
            var id = ctx.pathParam("id");
            ctx.contentType("html");
            ctx.result("<h1>" + id + "</h1>");
        });

        app.start(7070);
    }
}

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

http://localhost:7070/users/%3Cscript%3Ealert('attack!')%3B%3C%2Fscript%3E

Если мы доверимся данным от пользователей и откроем такой адрес, случится вот это:

XSS

В этом адресе закодирован JavaScript-код, который в оригинале выглядит так:

<script>
  alert('attack!');
</script>

Проблема в том, что код не отобразился, но попал в HTML страницы и выполнился. Так произошло, потому что для браузера такой JavaScript-код выглядит как часть страницы.

Если открыть получившийся HTML, то он будет выглядеть так:

<h1
  <script>alert('attack!');</script>
</h1>

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

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

Вернемся к коду из начала урока:

package org.example.hexlet;

import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
        });

        app.get("/users/{id}", ctx -> {
            var id = ctx.pathParam("id");
            ctx.contentType("html");
            ctx.result("<h1>" + id + "</h1>");
        });

        app.start(7070);
    }
}

Здесь мы выводим слаг без какой-либо предварительной обработки. В таком случае браузер пытается интерпретировать как HTML все, что похоже на HTML. Любой пользователь может внедрить на сайт исполняемый JavaScript-код без нашего ведома. Другими словами, мы проявили доверие к пользовательским данным и создали уязвимость.

Чтобы закрыть ее, нужно использовать не сами теги, а HTML-эквиваленты символов. Тогда код выше начнет выглядеть так:

<h1>
  &lt;script&gt;alert('attack!');&lt;/script&gt;
</h1>

Здесь мы заменили:

  • < на &lt;
  • > на &gt;

Это не экранирование, а именно замена спецсимволов на их HTML-эквиваленты. Если открыть браузер, то там мы увидим правильное отображение:

XSS

Замена символов на спецсимволы выполняется с помощью commons-text и других специальных библиотек. Один из вариантов выглядит так:

package org.example.hexlet;

import org.apache.commons.text.StringEscapeUtils;
import io.javalin.Javalin;

public class HelloWorld {
    public static void main(String[] args) {
        var app = Javalin.create(config -> {
            config.bundledPlugins.enableDevLogging();
        });

        app.get("/users/{id}", ctx -> {
            var id = ctx.pathParam("id");
            var escapedId = StringEscapeUtils.escapeHtml4(id);
            ctx.contentType("text/html");
            ctx.result(escapedId);
        });
        app.start(7070);
    }
}

Метод StringEscapeUtils.escapeHtml4() принимает на вход HTML и заменяет в нем все спецсимволы на их HTML-эквиваленты. Остальные символы остаются без изменений.

Через такую обработку нужно пропускать любые данные, которые мы выводим. Исключение составляют лишь ситуации, в которых мы точно знаем, что в данных есть HTML и мы его хотим отобразить. К таким данным могут относиться статьи в блоге, потому что они содержат HTML-часть. Например, так работает блог Хекслета.

Но даже внутрь безопасного HTML можно добавить вредоносный JavaScript-код. Вряд ли подобным будут заниматься сотрудники компании, но это могут сделать внешние авторы. Попробуем устранить эту уязвимость. В этом поможет механизм, который вырезает из HTML все <script> и другие опасные теги.

Рассмотрим пример с библиотекой java-html-sanitizer:

PolicyFactory policy = new HtmlPolicyBuilder()
    .allowElements("a")
    .allowUrlProtocols("https")
    .allowAttributes("href").onElements("a")
    .requireRelNofollowOnLinks()
    .toFactory();
String safeHTML = policy.sanitize(untrustedHTML);

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

Безопасность по умолчанию — это ключевой подход в создании надежных приложений. Один из способов ее обеспечить — использовать JTE или другие качественные шаблонизаторы. Они обрабатывают все данные внутри своих шаблонов и автоматически защищают от XSS-атак.

Рекомендуемые программы