Comments 67
document.write
.2. Не будет работать в комбинации с внешними скриптами вида
<script>var params={...};</script><script src="process_params.js"></script>
.</script> — это спецификация.- При препрецессинге кода проблем не будет,
т. к. там вряд ли будут штуки типа referer, поэтому и вероятность</script> мала. При шаблонизации на клиенте проблем тоже не будет,т. к. .innerText и .innerHTML позволяют вставлять</script> сколько угодно. А вот при шаблонизации на сервере да, будут проблемы. Но чтобы сказать об этом, не нужно было писать статью на 15 тыс символов. - Для решения проблемы серверной шаблонизации достаточно просто вставить обратный слэш (\),
т. к. </script> может вроде встретиться только в строках, комментах и регулярках, а там вставить слэш безопасно.
Проблема возникает только тогда, когда в тег <script>
подставляются автоматически генерируемые данные на основе пользовательского ввода (первичные, вторичные (из базы) или какого угодно порядка — не суть).
Честно говоря, не припоминаю ни одного случая, когда бы это было необходимо. Кодогенерация на основе пользовательских данных — это зло само по себе, и если уж в каком-то случае это необходимо, там кроме описанной проблемы будут еще сотни других. А данные — это данные, для них достаточно JSON-сериализации, а JSON можно, как справедливо замечено, поместить в атрибуты.
Да, JSON в атрибутах необходимо парсить. Ну а с вашей директивой safescript необходимо на лету откомпилировать Javascript. Парсинг JSON — очевидно менее ресурсоемкая операция. Тем более, многие фреймворки (angular, vue) умеют парсить JSON в атрибутах "из коробки".
По теме статьи, было:
<script>var x = <?php echo $x; ?></script>
стало:<?php $url = base64_encode("var x = " . $x); ?>
<script src="data:text/javascript;base64,<?php echo $url; ?>"></script>
И ваши проблемы решатся, нет?Подзравляю, вы открыли, что пользоваться eval с внешними данными не очень хорошая идея.
Жирный плюс!
По теме статьи, было… стало ...
Всё еще много проще — например было/стало:
-<script>var x = <%= JSON.stringify(...) %>; </script>
+<script>var x = <%=html= JSON.stringify(...) %>; </script>
Т.е. html=
это обёртка (функция, генератор, стрим), вызывающая htmlEncode, htmlEscape, и прочие подобные функции, делающие <
, >
, "
, &
и прочее, ещё никто не отменял.
Если foreign-input, embedded и прочее вставляется в html-разметку, т.е. результат сперва разбирается html-парсером, до того как будет собственно "исполнен" тег скрипт, полифилы и прочее ничего с собственно html-стандартом не имееющее.
И если забыть про правильный эскейп, то скрипт-тег будет далеко не единственной проблемой…
XSS, XFS, и прочие радости можно словить даже тупо на "сломаной" encoding...
К homm: это не имеет вообще ничего общего с "фундаментальной уязвимостью". Совсем.
делающие <, >, ", & и прочее, ещё никто не отменял.
И в результате у вас внутри строки Javascript будут именно <
, >
, "
, &
, а не <
, >
, "
, &
, которы были в исходной строке.
И если забыть про правильный эскейп, то скрипт-тег будет далеко не единственной проблемой…
Еще раз попробую донести свою мысль: скрипт-тег все еще останется проблемой, даже если не забыть про правильный эскейп. В случае тега script просто не предусмотрено никакого эскейпа для встраиваемого скрипта, на него просто наложены ограничения (причем с <!--
весьма извращенные), которых нет в Javascript.
И в результате у вас внутри строки Javascript будут именно<
,>
...
Прошу прощения, и правда попутал тут, — забыл про XHTML vs. CDATA (никогда не вставляю подобное прямо в скрипт).
скрипт-тег все еще останется проблемой, даже если не забыть про правильный эскейп
Попробую и я вам донести свою мысль — про правильный эскейп просто НЕЛЬЗЯ забывать, ни в коем случае.
Просто, если внутри тега ожидается CDATA, то и эскейпить нужно для CDATA.
Для HTML — HtmlEncode, для JS внутри HTML — HtmlJSEncode (например <a onclick="alert(<%=html-js= something(); %>)">
)…
И так далее.
А лучше просто класть foreign-inlput в предназначеные для этого теги (типа input
, textarea
и т.д.), и/или вовсе отдельным майм-стримом (т.е. application/json
).
про правильный эскейп просто НЕЛЬЗЯ забывать
Давайте еще раз. Скрипт-тег все еще останется проблемой, если вы НЕ ЗАБЫВАЕТЕ про правильный эскейп. Его для тега script просто не предусмотрено.
Пример который вы показали с onclick — это как раз пример того, где можно экранировать вставляемый скрипт, потому что атрибут onclick парсится точно так же как любые остальные атрибуты HTML и в нем может быть абсолютно любой контент, для его встраивания не нужно знать синтаксис скрипта.
В отличие от него, тег script парсится по своим, совершенно отдельным правилам. Он не позволяет вставить в себя произвольный Javascript, даже если он на 100% валидный Javascript. Он накладывает дополнительные требования на содержимое встриваемых скриптов, которые в случае Javascript могут быть соблюдены только парсингом и модификацией Javascript, а в случае встраивания скрипта на произвольном языке в общем случае не могут быть соблюдены.
А лучше просто класть foreign-inlput
Лучше или хуже это другой вопрос. Я не о лучших практиках говорю, а о проблеме в спецификации. Спецификация допускает встраивание в HTML языков, синтаксис которых позволяет выходить за пределы встраеваемого жлемента и не дает никаких способов этот синтаксис экранировать. Вместо этого она говорит: «там этого контента быть не должно. Как и кто его будет оттуда доставать? Да мне плевать.». Из-за этого часто возникают уязвимости уже в реальных приложениях, когда контент в теге скрипт генерируется на лету.
Его для тега script просто не предусмотрено.
см. ниже (просто попробуйте) ...
Заглянул в древние исходники своего js_encode (для utf-8 на сях), прекрасно работает много лет...
while (l--) {
switch ((c = (unsigned char)*p)) {
case '\\':
buf = "\\\\";
buflen = 2;
goto lab_enc;
case '\'':
buf = "\\'";
buflen = 2;
goto lab_enc;
case '"':
buf = "\\\"";
buflen = 2;
goto lab_enc;
case '\n':
buf = "\\n";
buflen = 2;
goto lab_enc;
case '\r':
buf = "\\r";
buflen = 2;
goto lab_enc;
case '<':
// wrap <!-- to <\!--, wrap </ to <\/ :
if (l && (*(p+1) == '!' || *(p+1) == '/')) {
buf = "<\\";
buflen = 2;
goto lab_enc;
}
break;
case '>':
// wrap --> to --\> :
if (p-1 > st && *(p-1) == '-' && *(p-2) == '-') {
buf = "\\>";
buflen = 2;
goto lab_enc;
}
break;
}
...
Да, для скрипт-тега, JS обертка должна, кроме всего прочего (кавычки и т.д.), еще и эскейпить /
, т.е. всего лишь:
<script> alert('<\/script>'); </script>
Символ /
встречается не только внутри строк, но и в самом скрипте (операция деления) и его экранированое с помощью \
приведет к порче скрипта.
Соответственно чтобы экранировать все символы /
внутри скрипта внутри строк нужно разобрать синтаксис срипта, найти все строки, модифицировать их, собрать обратно. В случае Javascript это несоразмерно сложно для такой простой задачи, для произвольного скрипта это невыполнимая задача, т.к. встроить можно что угодно, а синтаксиси чего угодно вы точно не сможете распарсить.
Символ / встречается не только внутри строк, но и в самом скрипте (операция деления)
Ну да, ииии? Это-то вам зачем экранировать? Или вы операцию деления тоже от пользователя (чужого кода) прямым способом у себя "вставляете"?!
Т.е. я вас правильно понимаю, что вы экранируете так (я утрирую дальше):
<%
MagicEncode(
JS_doing_WebServiceCall(
URL_Get(something),
XML_Get(Params), etc
)
);
%>
Т.е. одно магическое маскирование для JS (наружу), XML (веб-сервис), URL (something, опять для WS), и т.д. Всё одним MagicEncode?
Тогда у меня для вас плохая новость — это не камильфо, не правильно, закладывает множество скрытых мин (в долгосрочной перспективе). И вообще, так нельзя.
Никакой уязвимости нет, всё есть в спецификации. Просто надо экранировать не только кавычки, но и угловые скобки.
Если разработчик вставляет в JS-код, встроенный в страницу, неэкранированный текст, он сам виноват.
Хороший подход. А что если его вставляет не разработчик, а библиотека или сам барузер, как это описано в части А вы точно спецификация??
Просто надо экранировать не только кавычки, но и угловые скобки.
Это не всегда возможно, об этом я написал в части А вы точно спецификация?
Браузер вставляет ровно то, что ему сказали. Вместо закрывающего тега script мог бы быть любой другой.
Свойство innerText
вообще не стандартизовано и может работать так, как захочется конкретному браузеру.
В каких случаях экранирование невозможно?
Экранирование невозможно в случаях, когда:
- Вы не знаете на каком языке встраивается скрипт
- Синтаксис этого языка не предусматривает экранирования не специальных символов (то есть
\/
в строковых литералах будет означать\/
, а не\
). - У вас нет средств для синтаксического разбора и модификации исходного текста встраиваемого скрипта.
2. См. пункт 1.
3. Нельзя ожидать безопасности, встаивая на страницу скрипт с неопределённым содержанием. И когда это может понадобиться (именно не строку, а целый скрипт)?
- Вот вам известен язык в следующем теге?
<script type="application/x-my-templates">
Но вы хоть погуглить можете, а библиотеке, которая собирает из DOM-дерева HTML что делать? - ...
- Это необзательно неопределенное содержимое, просто модержмое может быть определено не в том месте, где формируется HTML-документ.
А что если его вставляет не разработчик, а библиотека
Позвольте вопрос, в целях повышения образованности, а библиотеки кто пишет?
Попробуйте в целях повышения образованности написать библиотеку, которая бы вставляла произвольный Javascript в тег <script>.
Задание со звездочкой: вставляла произвольный скрипт (не обязательно Javascript).
Если вам такое надо, попробуйте посмотреть в сторону RequireJS, Rollup или Webpack. Чтобы писать свое когда имеется уже написанное нужно иметь серьезные основания, если в вашем случае основание — это загрудка скрипта на произвольном языке то дерзайте, меня же на данный момент боже упаси от загрузки клиенту в браузер чего-то кроме JavaScript.
По существу статьи не понял:
1) как должен работать ваш safescript если я не держу текст скриптов в своей разметке (script src="...")?
2) валидация получаемой разметки вас не волнует?
3) у тега script есть еще целый ряд важных аттрибутов, как ваш safescript будет эмулирвать их поведение?
почему бы просто не экранировать сразу в шаблонизаторе как рекомендует стандарт
Это не всегда возможно, об этом я написал в части А вы точно спецификация?.
Смею предположить, что основной use case для такого экранирования — это всё-таки пользовательские данные, которые в шаблон попадают через json.encode
. В json нет арифметических операторов, поэтому всегда <!--
, -->
, <script
, </script>
будет внутри строк. Более того, json это не джаваскрипт, и чтобы стало совсем правильно, нужно делать var x = JSON.parse("...")
, у которого единственный аргумент сплошная строка. В остальных случаях, когда код пишется непосредственно разработчиком, спецификация вполне резонно предлагает избегать сомнительных конструкций.
А я вот всё в толк не возьму, что за проблему предлагается решать столь неоднозначным способом?
Возможность "вредоносным" пользователем встроить произвольное содержание в ваш документ — это скорее фундаментальный баг разработчика. Оное содержимое может оказаться в любом месте документа, ведь никто не мешает в качестве имени при регистрации указать:
Вася"><script>alert("Aloha!");</script>
и продолжить сей фрагмент остатком оригинального тэга. Т.е. это проблема всей разметки, а не исключительно тэга script.
Точно так же и в safescript можно включить любую ересь.
Правильно говорят — на странице должен быть только константный или тщательно проверенный и экранированный контент. Остальное данные и работа с DOM. Других способов защиты нет.
<sarcasm>
ну и далее нужна картинка про 15-й стандарт :)
</sarcasm>
Я давал выше ссылку на библиотеку Knockout.MVC. Там используется преобразование модели (содержащей пользовательские данные) в JSON поскольку JSON является валидным JS-литералом. Ваш трюк с закрытием кавычки тут ничего не сломает — кавычка будет экранирована в процессе преобразования в JSON.
А вот закрывающий тэг </script>
внутри строкового литерала, увы, сработает.
И кто защитит safescript от подобных проблем?
Экранирование <script> сложное, требует понимания синтаксиса встраиваемого скрипта, в общем случае не однозначное и не обратимое. Происходит по своим собственным законам.
Экранирование <safescript> простое, однозначное, обратимое, не требует знать ничего о встраиваевом скрипте, такое же как во всем остальном HTML.
И меня смутило долгое вступление про общие проблемы html, требующие аккуратного экранирования пользовательских данных.
Но если рассматривать safescript исключительно как разрешение конфликта парсеров html и script — то идея может и годная.
А заголовок всё же слишком громкий :)
Не проще вместо этого просто вынести скрипты в отдельные файлы?
Справедливости ради, в одном моем проекте встречается как раз такой юзкейс, и после этой статьи я его поправлю… но я не думаю, что это настолько распространено чтобы кричать о фундаментальной уязвимости в HTML.
В PHP слэши автоматически экранируются.
php -r "echo json_encode('</script>');"
"<\/script>"
php -r 'echo json_encode("<!--");'
"<!--"
Как бы да, но cама по себе она не влияет, а в сочетании с <script>
ей можно разве что закомментировать весь документ до конца. Воспользоваться уязвимостью и выполнить код не получится, по крайней мере я не нашел способа. Документ с валидной разметкой парсится корректно.
<script>
var a = <?= json_encode([
's' => "test <!-- <script>alert(1);",
't' => "--> </script>",
]) ?>;
</script>
<script>
console.log(a);
// Object { s: "test <!-- <script>alert(1);", t: "--> </script>" }
</script>
Если кто-то может встроить JS в код страницы, используя бекенд… затыкать это надо на стороне бекенда, или я туплю..?
Эта рекомендация меня умиляет. Тут делается сразу несколько наивных предположений:
Эти, как вы выразились, «наивные» предположения строятся на том, что разработчики стандарта ожидают, что вы будете следовать стандарту и соблюдать правила техники безопасности.
Всегда найдется идиот, который сможет выстрелить себе в ногу. Но соблюдение правил ТБ хотя бы уменьшает их количество.
Полагать, что разработчики стандарта должны были подумать о естественных идиотах и их проблемах со стрельбой в ногу… наивно. По меньшей мере наивно.
В этом случае, как и написано в спецификации, всего-то нужно заменить все "<!--" на "<!--", "<script" на "<\script", а "</script" на "<\/script".
Можно проще, достаточно заэскейпить символ "<". В документации к Redux есть пример:
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
</script>
Если этого кажется мало, всегда можно взять более продвинутую библиотеку.
Я, конечно, могу сказать глупость, но:
<script>
//<![CDATA[
var a = 'Consider this string: <!--';
var b = '<script>';
//]]>
</script>
Разве нет?
1) Токенизатор script data stage
2) Переключатель токенизатора (восьмой пункт)

У меня статья вызвала недоумение. Защита от людей не знающих хтмл?
«В свою очередь, Javascript — это самостоятельный язык с собственным синтаксисом, он, вообще говоря, никаким специальным образом не рассчитан на то, что будет встроен в HTML.»
То есть язык, разработанный в Mosaic специально для браузера и для работы в вебе, никак не рассчитан на работу в связке с HTML?
Поздравляю, вы открыли XSS.
Эта и многие другие фундаментальные проблемы html уже решены в xhtml. Но индустрия выбрала вариант стандартизировать говнокод.
<script source="./script.js" />
Правда смотрится это довольно грязно
Кстати, если уж хакиш-вэй так хочется embedded, то чем вот это вот не устраивает?
<var data="surprise!</script><script>alert("whoops!")</script>">
</var><script>
var s = (function(){
var s = document.currentScript;
if (!s) {s = document.getElementsByTagName('script'); s = s[s.length-1];}
return s.previousSibling.getAttribute('data');
})();
console.log(s);
</script>
Естественно обернув полифилом, нормальной функцией в АПИ и т.д.
Оно не крадет ID, и работает вроде везде...
Фундаментальная уязвимость HTML при встраивании скриптов