Pull to refresh

Comments 67

1. Не будет работать document.write.
2. Не будет работать в комбинации с внешними скриптами вида <script>var params={...};</script><script src="process_params.js"></script>.

Совершенно верно, <safescript> это аналог <script defer>. Забыл об этом сказать.

Статья не очень.

  1. <​/script> — это спецификация.
  2. При препрецессинге кода проблем не будет, т. к. там вряд ли будут штуки типа referer, поэтому и вероятность <​/script> мала. При шаблонизации на клиенте проблем тоже не будет, т. к. .innerText и .innerHTML позволяют вставлять <​/script> сколько угодно. А вот при шаблонизации на сервере да, будут проблемы. Но чтобы сказать об этом, не нужно было писать статью на 15 тыс символов.
  3. Для решения проблемы серверной шаблонизации достаточно просто вставить обратный слэш (\), т. к. <​/script> может вроде встретиться только в строках, комментах и регулярках, а там вставить слэш безопасно.
UFO just landed and posted this here

Проблема возникает только тогда, когда в тег <script> подставляются автоматически генерируемые данные на основе пользовательского ввода (первичные, вторичные (из базы) или какого угодно порядка — не суть).


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


Да, JSON в атрибутах необходимо парсить. Ну а с вашей директивой safescript необходимо на лету откомпилировать Javascript. Парсинг JSON — очевидно менее ресурсоемкая операция. Тем более, многие фреймворки (angular, vue) умеют парсить JSON в атрибутах "из коробки".

Совершенно верно. Вставляйте в safescript :-)

Подзравляю, вы открыли, что пользоваться eval с внешними данными не очень хорошая идея.

По теме статьи, было:
<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, и прочие подобные функции, делающие &lt;, &gt;, &quot;, &amp; и прочее, ещё никто не отменял.
Если foreign-input, embedded и прочее вставляется в html-разметку, т.е. результат сперва разбирается html-парсером, до того как будет собственно "исполнен" тег скрипт, полифилы и прочее ничего с собственно html-стандартом не имееющее.


И если забыть про правильный эскейп, то скрипт-тег будет далеко не единственной проблемой…
XSS, XFS, и прочие радости можно словить даже тупо на "сломаной" encoding...


К homm: это не имеет вообще ничего общего с "фундаментальной уязвимостью". Совсем.

делающие <, >, ", & и прочее, ещё никто не отменял.

И в результате у вас внутри строки Javascript будут именно &lt;, &gt;, &quot;, &amp;, а не <, >, ", &, которы были в исходной строке.


И если забыть про правильный эскейп, то скрипт-тег будет далеко не единственной проблемой…

Еще раз попробую донести свою мысль: скрипт-тег все еще останется проблемой, даже если не забыть про правильный эскейп. В случае тега script просто не предусмотрено никакого эскейпа для встраиваемого скрипта, на него просто наложены ограничения (причем с <!-- весьма извращенные), которых нет в Javascript.

И в результате у вас внутри строки Javascript будут именно &lt;, &gt; ...

Прошу прощения, и правда попутал тут, — забыл про 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?


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

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

Ну вы же сами показали пример, что есть.

Где я показывал такой пример?


Если вы про Knockout.MVC — то они просто забыли про экранирование, исправление-то той строчки тривиально (например, подойдет Replace("<", "\\u003c")).

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

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


Просто надо экранировать не только кавычки, но и угловые скобки.

Это не всегда возможно, об этом я написал в части А вы точно спецификация?

Браузер вставляет ровно то, что ему сказали. Вместо закрывающего тега script мог бы быть любой другой.
Свойство innerText вообще не стандартизовано и может работать так, как захочется конкретному браузеру.
В каких случаях экранирование невозможно?

Экранирование невозможно в случаях, когда:


  1. Вы не знаете на каком языке встраивается скрипт
  2. Синтаксис этого языка не предусматривает экранирования не специальных символов (то есть \/ в строковых литералах будет означать \/, а не \).
  3. У вас нет средств для синтаксического разбора и модификации исходного текста встраиваемого скрипта.
1. Это в каких же случаях язык скрипта неизвестен (бонусный вопрос: когда он отличается от JS)?
2. См. пункт 1.
3. Нельзя ожидать безопасности, встаивая на страницу скрипт с неопределённым содержанием. И когда это может понадобиться (именно не строку, а целый скрипт)?
  1. Вот вам известен язык в следующем теге? <script type="application/x-my-templates"> Но вы хоть погуглить можете, а библиотеке, которая собирает из DOM-дерева HTML что делать?
  2. ...
  3. Это необзательно неопределенное содержимое, просто модержмое может быть определено не в том месте, где формируется HTML-документ.

А зачем библиотеке автоматически формировать <script type="application/x-my-templates">? Обычно наоборот происходит: пишет такие шаблоны программист, а библиотека их парсит.

А что если его вставляет не разработчик, а библиотека


Позвольте вопрос, в целях повышения образованности, а библиотеки кто пишет?

Попробуйте в целях повышения образованности написать библиотеку, которая бы вставляла произвольный Javascript в тег <script>.


Задание со звездочкой: вставляла произвольный скрипт (не обязательно Javascript).

Зачем мне писать библиотеку? Тем более такую странную.

Если вам такое надо, попробуйте посмотреть в сторону RequireJS, Rollup или Webpack. Чтобы писать свое когда имеется уже написанное нужно иметь серьезные основания, если в вашем случае основание — это загрудка скрипта на произвольном языке то дерзайте, меня же на данный момент боже упаси от загрузки клиенту в браузер чего-то кроме JavaScript.

По существу статьи не понял:
1) как должен работать ваш safescript если я не держу текст скриптов в своей разметке (script src="...")?
2) валидация получаемой разметки вас не волнует?
3) у тега script есть еще целый ряд важных аттрибутов, как ваш safescript будет эмулирвать их поведение?
Спасибо за статью, привычка полагаться на htmlescape действительно может подвести. Но решение вызывает определённые сомнения: почему бы просто не экранировать сразу в шаблонизаторе как рекомендует стандарт, выдавая нативный тег script? Ограничиваясь по сути имплементацией темплейттега в джанге.
почему бы просто не экранировать сразу в шаблонизаторе как рекомендует стандарт

Это не всегда возможно, об этом я написал в части А вы точно спецификация?.

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

Смею предположить, что основной 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.

ок, ок… наверно я что-то делаю не так, но мне за последние 15 лет ни разу не пришлось использовать встраиваемые скрипты с небезопасным содержимым…
И меня смутило долгое вступление про общие проблемы html, требующие аккуратного экранирования пользовательских данных.
Но если рассматривать safescript исключительно как разрешение конфликта парсеров html и script — то идея может и годная.

А заголовок всё же слишком громкий :)

Не проще вместо этого просто вынести скрипты в отдельные файлы?

А я что-то писал про safescript?
Как уже написали выше — уязвим будет только тот клиент, который генерирует содержимое тега <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>
Для DoS атаки вполне годная уязвимость.

Если кто-то может встроить 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) Переключатель токенизатора (восьмой пункт)

Когда-то я рисовал картинку для наглядности
image

У меня статья вызвала недоумение. Защита от людей не знающих хтмл?
«В свою очередь, Javascript — это самостоятельный язык с собственным синтаксисом, он, вообще говоря, никаким специальным образом не рассчитан на то, что будет встроен в HTML.»


То есть язык, разработанный в Mosaic специально для браузера и для работы в вебе, никак не рассчитан на работу в связке с HTML?

Что вы имеете в виду под работой в связке?

Что вы имеете в виду под работай в связке? Я написал на что конкретно он не расчитан: на встраивание в HTML.

Нет, я как раз пытаюсь закрыть XSS

Эта и многие другие фундаментальные проблемы html уже решены в xhtml. Но индустрия выбрала вариант стандартизировать говнокод.

> Как вы уже поняли, способа безопасно вставить Javascript в HTML нет.

<script source="./script.js" />

Тогда уж
<script src="https://domain.tld/script.js"></script>
UFO just landed and posted this here
безобидный тэг который обычно в списке разрешенных тэгов на равные с , и т.д., но в эпоху популярности чатов, можно было похулиганить — отправляешь открытый тэг, а через 10-20 сообщений закрытый и весь чат начинал «плыть».
Правда смотрится это довольно грязно

Кстати, если уж хакиш-вэй так хочется embedded, то чем вот это вот не устраивает?


<var data="surprise!</script><script>alert(&quot;whoops!&quot;)</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, и работает вроде везде...

Спасибо интересные замечания. Скрипт в теле документа я могу представить только для передачи состояния серверного объекта JSON.stringify/JSON.parse. Но в этом случае лучше просто дополнительно ввести операцию кодировния/декодирования некоторых сущностей. Они абсолютно однозначны тогода будут. И синхронны
Даже вот так как оказалось я где-то закопипастил
window.__PRELOADED_STATE__ = ${JSON.stringify(store.getState()).replace(/</g, '\\u003c')}
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Sign up to leave a comment.

Articles