Когда вы выводите содержимое JSON через Javascript, должен ли я бежать на сервере или на стороне клиента?

У меня есть приложение, которое состоит из API REST на стороне сервера, написанного на PHP, и некоторого Javascript на стороне клиента, который использует этот API и использует JSON, который он создает для отображения страницы. Итак, довольно типичная настройка.

Данные, предоставленные REST API, являются "ненадежными" в том смысле, что он извлекает предоставленный пользователем контент из базы данных. Так, например, он может получить что-то вроде:

{
    "message": "<script>alert("Gotcha!")</script>"
}

Очевидно, что если бы мой клиентский код отображал это непосредственно на странице DOM, я создал уязвимость XSS. Таким образом, этот контент должен быть сначала экранирован HTML.

Вопрос в том, когда выводить ненадежный контент, должен ли я избегать содержимого на стороне сервера или на стороне клиента? I.e., должен ли мой API вернуть исходный контент, а затем сделать его ответственным за код Javascript для того, чтобы избежать специальных символов, или мой API вернет "безопасный" контент:

{
    "message": "&lt;script&gt;alert(&#039;Gotcha!&#039;);&lt;\/script&gt;"
}

который уже был экранирован?

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

Какой подход правильный?

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

Обновление. Я посмотрел, что делают другие люди, и кажется, что некоторые API REST имеют тенденцию отправлять "небезопасные" JSON. API Gitter фактически отправляет оба, что интересно:

[
    {
        "id":"560ab5d0081f3a9c044d709e",
        "text":"testing the API: <script>alert('hey')</script>",
        "html":"testing the API: &lt;script&gt;alert(&#39;hey&#39;)&lt;/script&gt;",
        "sent":"2015-09-29T16:01:19.999Z",
        "fromUser":{
            ...
        },"unread":false,
        "readBy":0,
        "urls":[],
        "mentions":[],
        "issues":[],
        "meta":[],
        "v":1
    }
]

Обратите внимание, что они отправляют исходное содержимое в ключ text, а затем версию с экранированным HTML в html. Неплохая идея, ИМО.

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

Ответ 1

Побег только на стороне клиента.

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

Причиной не сбежать на стороне сервера является разделение проблем: сервер не должен предполагать, что клиент намерен отображать данные как HTML. Вывод сервера должен быть как можно меньше медиа-нейтраль (с учетом ограничений JSON и структуры данных, конечно), так что клиент может наиболее легко преобразовать его в любой формат.

Ответ 2

Для выхода на выход:

Я предлагаю прочитать этот шпаргалку XSS Filter Evasion.

Чтобы правильно предотвратить пользователя, вам лучше не только сбежать, но и перед тем, как его убрать, отфильтруйте его с помощью соответствующей библиотеки анти-XSS. Как htmLawed, или HTML Purifier, или любой из этой темы.

ИМХО санация должна проводиться на вводимых пользователем данных всякий раз, когда вы собираетесь показывать их обратно в веб-проекте.

я должен избегать содержимого на стороне сервера или на стороне клиента? Т.е. должен ли мой API возвращать необработанный контент, а затем возложить на клиентский код Javascript ответственность за экранирование специальных символов, или же мой API должен возвращать "безопасный" контент:

Лучше вернуть уже сбежавший, а xss очищенный контент, так:

  1. Возьмите необработанные данные и очистите их от xss на сервере
  2. Побег это
  3. Вернуться к JavaScript

Кроме того, вы должны заметить одну важную вещь, такую как загрузка вашего сайта и баланс чтения/записи: например, если ваш клиент вводит данные один раз и вы собираетесь показывать эти данные 1M пользователям, что вы предпочитаете: запускать логику защиты один раз перед записью (защита на входе) по миллиону раз при каждом чтении (защита на выходе)?

Если вы собираетесь показывать 1K постов на странице и избегать каждого на клиенте, насколько хорошо это будет работать на клиентском мобильном телефоне? Этот последний поможет вам выбрать, где защищать данные на клиенте или на сервере.

Ответ 3

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

Почему бы не избежать клиентской стороны?

Я бы сказал, что побег на уровне javascript не является хорошей идеей. Просто проблема с моей головы была бы, если бы была ошибка в санировке script, она не запускалась, а затем опасный script был бы разрешен для запуска. Таким образом, вы ввели вектор, в котором злоумышленник может попытаться создать вход для разрушения дезинфицирующего средства JS, так что их простой script разрешен для запуска. Я также не знаю никаких встроенных библиотек AntiXSS, которые запускаются в JS. Я уверен, что кто-то сделал один или может его создать, но есть примеры на стороне сервера, которые немного заслуживают доверия. Также стоит упомянуть, что писать дезинфицирующее средство в JS, которое работает для всех браузеров, не является тривиальной задачей.

Хорошо, что, если вы бежите на обоих?

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

Почему серверная сторона достаточно хороша?

Недостаточно поддержки на стороне сервера. Ваш вопрос о том, чтобы сделать это как можно дольше, имеет смысл, но я думаю, что недостатки побега на стороне клиента перевешиваются тем крошечным преимуществом, которое вы можете получить, сделав это. Где угроза? Если злоумышленник существует между вашим сайтом и клиентом, клиент уже скомпрометирован, так как он может просто отправить пустой html файл со своими script, если они захотят. Вам нужно сделать все возможное, чтобы отправить что-то безопасное, а не просто отправлять инструменты для работы с вашими опасными данными.

Ответ 4

TL;DR; Если ваш API должен передавать информацию о форматировании, он должен выводить строки в кодировке HTML. Предостережение: любой потребитель должен будет доверять вашему API, чтобы не выводить вредоносный код. Политика безопасности контента также может помочь в этом.

Если ваш API должен выводить только простой текст, то HTML кодирует на стороне клиента (так как < в простом тексте также означает < в любом выводе).

Не слишком долго, не дочитал

Если вы владеете как API, так и веб-приложением, то в любом случае это приемлемо. Пока вы не выводите JSON на HTML-страницы без шестнадцатеричного кодирования сущностей, например:

<%
payload = "[{ foo: '" + foo + "'}]"
%>
    <script><%= payload %></script>

тогда не имеет значения, изменяется ли код на вашем сервере & на &amp; или код в браузере меняется & к &amp; ,

Давайте возьмем пример из вашего вопроса:

[
    {
        "id":"560ab5d0081f3a9c044d709e",
        "text":"testing the API: <script>alert('hey')</script>",
        "html":"testing the API: &lt;script&gt;alert(&#39;hey&#39;)&lt;/script&gt;",
        "sent":"2015-09-29T16:01:19.999Z",

Если вышеупомянутое возвращается с api.example.com, и вы вызываете его с www.example.com, так как вы контролируете обе стороны, вы можете решить, хотите ли вы взять простой текст, " text " или форматированный текст, " html ".

Важно помнить, что любые переменные, вставленные в html, были здесь на стороне сервера в кодировке HTML. А также предположим, что было выполнено правильное кодирование JSON, которое предотвращает ломку любых символов кавычки или изменение контекста JSON (это не показано выше для простоты).

text будет вставлен в документ с использованием Node.textContent и html как Element.innerHTML. Использование Node.textContent заставит браузер игнорировать любой формат HTML и сценарий, которые могут присутствовать, поскольку такие символы, как <, буквально воспринимаются как выводимые как < на странице.

Обратите внимание, что ваш пример показывает, что пользовательский контент вводится как скрипт. т.е. пользователь ввел <script>alert('hey')</script> в ваше приложение, оно не генерируется API. Если ваш API действительно хочет выводить теги как часть своей функции, то он должен поместить их в JSON:

"html":"<u>Underlined</u>"

И тогда ваш text должен будет только выводить текст без форматирования:

"text":"Underlined"

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

Однако, если третье лицо потребляет ваш API, то они могут Node.textContent получить данные из вашего API в виде простого текста, потому что тогда они могут установить Node.textContent (или HTML-кодирование его) на стороне клиента, зная, что он безопасно. Если вы вернете HTML, тогда ваш потребитель должен поверить, что ваш HTML не содержит вредоносных скриптов.

Так что, если вышеуказанный контент взят с сайта api.example.com, но ваш потребитель является сторонним сайтом, например, www.example.edu, то им может быть удобнее воспринимать text а не HTML. В этом случае может потребоваться более детально определить ваш вывод, а не выводить

"text":"Thank you Alice for signing up."

Вы бы вывели

[{ "name", "alice",
"messageType": "thank_you" }]

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

"text":"Thank you Alice for signing up."

и они хотели показать имена жирным шрифтом, для них было бы очень сложно выполнить это без сложного анализа. Однако с определением выходных данных API на уровне детализации потребитель может взять соответствующие фрагменты выходных данных, например, переменные, а затем применить свое собственное форматирование HTML, не доверяя своему API только выводить полужирные теги (<b>), а не выводить вредоносный JavaScript (от пользователя или от вас, если вы действительно были злонамеренным или если ваш API был взломан).