Опасность использования v-html в приложениях Vue

Spread the love

Перевод: Bartosz Salwiczek Danger of using v-html in Vue applications

Директива v-html используется для изменения внутреннего HTML-кода элемента DOM. Это может показаться безопасным и очень удобным, но это первое, что ищут злоумышленники, пытаясь взломать ваш сайт. В этой статье я покажу потенциальную опасность, связанную с использованием v-html, и покажу альтернативные подходы к устранению этой дыры в безопасности.

Уязвимое приложение

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

<template>
  <div id="app">
    <div v-for="comment in comments" :key="comment.id">
      <div v-html="comment.body"></div>
    </div>

    <textarea v-model="commentBody"></textarea>

    <button @click="addComment">Add comment</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      comments: [],
      commentBody: "",
    };
  },
  methods: {
    addComment() {
      const id = this.comments.length;
      const body = this.commentBody;
      const comment = { id, body };
      
      this.comments.push(comment);
      
      // await fetch("https://myserver.com/", { method: 'POST', body: JSON.stringify(comment) });
      this.commentBody = "";
      
    },
    fetchComments() {
      // const response = await fetch("https://myserver.com/")
      // this.comments = await response.json();
      this.comments = [
        {
          id: 0,
          body: "first comment",
        }
      ];
    }
  },
  mounted() {
    this.fetchComments();
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  display: flex;
  flex-direction: column;
  width: 50%;
  margin: 0 auto;
}
textarea {
  margin-top: 100px;
}
</style>

Вы можете поиграть с ним на CodePen

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

Автор хотел, чтобы пользователи могли добавлять собственные стили в комментарии (например, он хотел, чтобы <b>Important</b> стало Important).

«Похоже на идеальное применение v-html!» — подумал он.

Но он не знал, что эта маленькая функция делает его приложение уязвимым для XSS-атак.

Межсайтовый скриптинг (XSS) позволяет одному пользователю внедрять вредоносный скрипт в приложение и запускать его на компьютере другого пользователя, когда они открывают зараженную страницу.

Как его взломать?

Давайте посмотрим, как приложение можно взломать. Благодаря тому, что комментарии интерпретируются как HTML, у злоумышленника есть масса возможностей. Например, он мог отправить такой комментарий:

Hello! <img style=”display:none” src=”https://hacker-website.com” />

“Hello!” будет отображаться как комментарий, однако тег img будет интерпретирован как действительный HTML. Обычный пользователь, посещающий веб-сайт, не догадается, что его браузер в фоновом режиме отправит запрос на https://hacker-website.com, поскольку умный злоумышленник присвоил изображению стиль display:none.

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

  • Украсть учетные данные из приложения
  • Украсть содержимое файлов cookie
  • Отправить пользователю вредоносный скрипт
  • И многое другое…

Как это исправить?

Предположим, вам нужно исправить это приложение. Что можно сделать?
Есть как минимум есть три возможных способа исправить эту уязвимость:

1. Избавьтесь от тега v-html

Вы можете отобразить тело комментария, используя {{ }}:

<div> {{ comment.body }} </div>

или используя v-text:

<div v-text="comment.body"></div>

Таким образом, Vue Engine  очистит (sanitize) содержимое, предоставленное в comment.body, и проблема исчезнет.

Как и потенциальный style, которого так хотел автор.

Санитарная обработка (sanitizing) контента может означать замену опасных знаков (таких как < > &) на их безопасные замены (&lt; &gt; &amp;) или удаление некоторых опасных тегов из контента (таких как <script> или <img>).

2. Очистка (Sanitize) тела комментария контролируемым образом

Вы можете самостоятельно очистить (sanitize) контент, предоставленный пользователями, и избавиться от всех опасных тегов и атрибутов, которые могут использовать злоумышленники, и оставить только те, которые безопасны и важны для вашего сайта.
Но вы никогда не должны внедрять алгоритмы безопасности самостоятельно (если вы не являетесь экспертом)! Лучше доверять существующим, проверенным в боях, решениям.

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

Если вы хотите разрешить пользователям вводить только теги <b> и  <i>  и запретить все атрибуты, вы можете написать:

import DOMPurify from 'dompurify';

...

methods: {
  addComment() {
    const id = this.comments.length;

    const comment = { id, body: this.clearBody(this.commentBody) };
    this.comments.push(comment);

    // await fetch("https://myserver.com/", { method: 'POST', body: JSON.stringify(comment) });
    this.commentBody = "";

  },
  fetchComments() {
    // const response = await fetch("https://myserver.com/")
    // this.comments = await response.json();
    this.comments = [
      {
        id: 0,
        body: "first comment",
      }
    ];
    this.comments = this.comments.map((comment) => { id: comment.id, body: this.clearBody(comment.body) });
  },
  clearBody(body) {
    const sanitazedBody = DOMPurify.sanitize(this.currentComment, {ALLOWED_TAGS: ['b', 'i'], ALLOWED_ATTR: []});
    return sanitizedBody;
  }
}

Основная часть — это внутренняя функция clearBody, которая вызывается при добавлении новых комментариев, а также после получения комментариев от API (что еще важнее). Внутри него мы используем DOMPurify.sanitize с настраиваемыми параметрами для очистки контента.

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

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

3. Вместо v-html используйте markdown

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

Markdown — это удобный язык для форматирования обычного текста. Он преобразуется в другой язык, такой как HTML, и отображается на веб-сайтах. Например, используется в файлах README.md. Чтобы узнать больше о синтаксисе Markdown, посетите этот веб-сайт.

Опять же, лучше использовать проверенную библиотеку, например Marked.

Предупреждение. В документации Marked мы можем прочитать, что библиотека не очищает выходной HTML-код, поэтому, чтобы избежать дыр в безопасности, вам все равно следует очищать его с помощью нашего хорошего друга DOMPurify.sanitize().

Использование Marked в нашем приложении:

import DOMPurify from 'dompurify';
import marked from 'marked';

...

methods: {
  addComment() {
    const id = this.comments.length;

    const comment = { id, body: this.transformAndClearBody(this.commentBody) };
    this.comments.push(comment);

    // await fetch("https://myserver.com/", { method: 'POST', body: JSON.stringify(comment) });
    this.commentBody = "";

  },
  fetchComments() {
    // const response = await fetch("https://myserver.com/")
    // this.comments = await response.json();
    this.comments = [
      {
        id: 0,
        body: "first comment",
      }
    ];
    this.comments = this.comments.map((comment) => { id: comment.id, body: this.transformAndClearBody(comment.body) });
  },
  transformAndClearBody(body) {
    const htmlBody = marked(body);
    const sanitizedBody = DOMPurify.sanitize(htmlBody);
    return sanitizedBody;
  }
}

Мы переименовали clearBody() в transformAndClearBody() и включили туда преобразование markdown в HTML с использованием метода marked  из пакета marked .

Таким образом, пользователь может стилизовать свой текст, используя известный синтаксис Markdown.

Примечание. Как и в 2., вы можете очищать контент на серверной части перед его сохранением в базе данных.

Заключение

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

Если вы считаете, что вам нужен v-html, подумайте еще три раза, прежде чем внедрять его.

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

Если он должен отображаться, используйте средства очистки, чтобы убедиться, что оно не делает ваше приложение уязвимым для XSS-атак.

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

Была ли вам полезна эта статья?
[3 / 3.7]

Spread the love
Подписаться
Уведомление о
guest
0 Комментарий
Inline Feedbacks
View all comments