Client-Side Fundamental
  • Добро пожаловать
  • Глава 1 - Начало работы с XSS
    • Браузерная модель безопасности
    • Знакомимся с уязвимостью XSS
    • Более глубокое понимание XSS
    • Опасный псевдопротокол javascript
  • Глава 2 - Защита и Обход для XSS
    • Первая линия обороны от XSS - Sanitization
    • Вторая линия обороны от XSS - CSP (Content Security Policy)
    • Третья линия обороны против XSS - сокращение области воздействия
    • Последние методы защиты от XSS - Trusted Types и встроенный Sanitizer API
    • Обход защитных мер - Обычные способы обхода CSP
    • Обход защитных мер - Mutation XSS
    • Самая опасная XSS - Universal XSS
  • Глава 3 - Атаки без JavaScript
    • Кто сказал, что для атаки обязательно выполнять JavaScript?
    • Prototype Pollution - Эксплуатация цепочки прототипов
    • Может ли HTML влиять на JavaScript - Введение в DOM clobbering
    • Template Injection in Frontend - CSTI
    • CSS Injection - Атака с использованием только CSS (Часть 1)
    • CSS Injection - Атака с использованием только CSS (Часть 2)
    • Можно ли атаковать, используя только HTML
  • Глава 4 - Межсайтовые атаки
    • Same-origin Policy и Same-Site
    • Введение в Cross-Origin Resource Sharing (CORS)
    • Проблемы Cross-Origin безопасности
    • Cross-Site Request Forgery (CSRF)
    • Спаситель от CSRF - Same-site cookie
    • От same-site до главного site
    • Интересная и практичная Cookie Bomb
  • Глава 5 - Другие интересные темы
    • То, что вы видите, это не то, что вы получаете - Clickjacking
    • Эксплуатация MIME Sniffing
    • Атаки на цепочку поставок во фронтенде - Attacking Downstream from Upstream
    • Атаки на веб-фронтенд в Web3
    • Самая интересная атака на побочные каналы фронтенда - XSLeaks (Часть 1)
    • Самая интересная атака на побочные каналы фронтенда - XSLeaks (Часть 2)
Powered by GitBook
On this page
  • Самая Базовая Мера: Кодирование
  • Обработка HTML
  • DOMPurify
  • Правильная библиотека, неверное использование
  • Заключение
  1. Глава 2 - Защита и Обход для XSS

Первая линия обороны от XSS - Sanitization

PreviousОпасный псевдопротокол javascriptNextВторая линия обороны от XSS - CSP (Content Security Policy)

Last updated 8 months ago

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

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

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

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

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

// This is a feature that users can embed their favorite YouTube videos in their profilepage
const url = "value from user"; // Make sure it's YouTube video URL

if (url.startsWith("https://www.youtube.com/watch")) {
  document.querySelector("iframe").src = url;
}

Проблема в этом коде заключается в том, что проверка URL недостаточно строгая, позволяющая пользователям вводить ссылки на видео, такие как , и такая ссылка приведет к странице настроек аккаунта.

Но это не звучит слишком плохо, верно? Это просто другая страница YouTube, и она все еще должна быть в рамках YouTube.

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

Например, если перенаправляет на , то я могу использовать этот URL, чтобы в iframe отобразился мой блог, а не ожидаемое видео с YouTube.

И действительно, на YouTube в настоящее время есть URL с open redirect, которые можно использовать. Однако, поскольку они могут быть исправлены в любое время, я не буду предоставлять здесь эти URL.

Если вам нужно проверить URL, рекомендуемый подход - использовать new URL() для разбора и смотреть на возвращаемое значение. Этот метод намного надежнее, чем простое сравнение строк или RegExp.

Самая Базовая Мера: Кодирование

Почему работает атака XSS?

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

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

В разработке интерфейсов при выводе ввода пользователя на экран в JavaScript следует использовать innerText или textContent вместо innerHTML. Таким образом, ввод пользователя будет интерпретироваться как обычный текст.

React и Vue имеют встроенные аналогичные функции. Основная логика:

> По умолчанию все, что отображается, будет восприниматься как обычный текст. Если требуется отобразить HTML, используйте специальные методы, такие как dangerouslySetInnerHTML или v-html.

Однако в наши дни многие бэкэнды непосредственно сами не выводят контент. Для этого они используют шаблонизаторы. Например, широко используемые handlebarsjs по умолчанию считают {{ name }} закодированным выводом. Для вывода необработанного содержимого требуются три фигурные скобки: {{{ vulnerable_to_xss }}}.

В шаблонизаторе Laravel, {{ $text }} кодируется, а {!! $text !!} - нет. Интересно, служит ли знак восклицания предупреждением: "Эй, будьте осторожны при использовании этого".

Некоторые шаблонизаторы используют фильтры. Например, в Python Jinja, {{ text }} кодируется, в то время как {{ text | safe }} означает, что содержимое безопасно и может быть непосредственно выведено в исходном формате.

Таким образом, при написании кода лучше всего использовать безопасный подход по умолчанию. Обратите внимание на небезопасные части (включая упомянутую ранее проблему с <a href>, которая требует особого внимания).

Когда мы будем использовать небезопасные методы вывода?

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

Так как мы работаем с этой ситуацией? Здесь вступает в игру санитизация.

Обработка HTML

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

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

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

Например, Python имеет библиотеку под названием BeautifulSoup, которая может разбирать веб-страницы и обычно используется для веб-скрейпинга. Однако она не предназначена для санитизации, поэтому ее использование может вызвать проблемы.

Хоть она не предназначена для санитизации, она используется для разбора веб-страниц, верно? Так почему мы не можем использовать ее?

Позвольте продемонстрировать:

from bs4 import BeautifulSoup

html = """  
<div>    
    test    
    <script>alert(1)</script>    
    <img src=x onerror=alert(1)>  
</div>
"""

tree = BeautifulSoup(html, "html.parser")

for element in tree.find_all():
    print(f"name: {element.name}")
    print(f"attrs: {element.attrs}")

Вывод этой программы:

name: div
attrs: {}
name: script
attrs: {}
name: img
attrs: {'src': 'x', 'onerror': 'alert(1)'}

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

from bs4 import BeautifulSoup

html = """  
<div>    
    test    
    <!--><script>alert(1)</script>-->  
</div>
"""

tree = BeautifulSoup(html, "html.parser")

for element in tree.find_all():
    print(f"name: {element.name}")
    print(f"attrs: {element.attrs}")

Выходные данные:

name: div
attrs: {}

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

Обход основан на различиях в разборе между браузерами и BeautifulSoup для следующих HTML:

<!--><script>alert(1)</script>-->

Парсер HTML BeautifulSoup рассматривает это как комментарий, заключенный в <!-- и -->, поэтому он не будет разбирать ни теги, ни атрибуты.

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

Кстати, если вы переключите парсер BeautifulSoup на lxml, он все равно не сможет правильно его разобрать. Но если вы переключитесь на html5lib, то он правильно разберет его как <script>. Однако, с html5lib могут возникнуть другие проблемы.

Так что, есть ли какие-нибудь рекомендуемые библиотеки, специально разработанные для санитизации? Да, мне как раз известна одна.

DOMPurify

Базовое использование DOMPurify выглядит так:

const clean = DOMPurify.sanitize(html);

Он делает многое вне поля нашего зрения и не только удаляет опасные теги и атрибуты, но и защищает от других атак, таких как DOM-Clobbering. Он очень тщательно работает.

DOMPurify по умолчанию разрешает только безопасные теги, такие как <h1>, <p>, <div> и <span>. Он также удаляет все обработчики событий и очищает ранее упомянутый псевдо-протокол javascript:, гарантируя, что любой ваш HTML-ввод не приведет к XSS в стандартном сценарии.

Но стоит отметить, что тег <style> включен по умолчанию, и мы обсудим связанные с ним риски позже.

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

const config = {
    ADD_TAGS: ["iframe"],
    ADD_ATTR: ["src"],
};

let html = "<div><iframe src=javascript:alert(1)></iframe></div>";
console.log(DOMPurify.sanitize(html, config)); // <div><iframe></iframe></div>

html = "<div><iframe src=https://example.com></iframe></div>";
console.log(DOMPurify.sanitize(html, config)); // <div><iframe src="https://example.com"></iframe></div>

Из приведенного выше примера видно, что даже если мы разрешаем атрибут src тега iframe, опасное содержимое все равно будет автоматически отфильтровано. Это связано с тем, что мы разрешаем только атрибут src и не разрешаем использование javascript:.

Однако, если вы хотите разрешить некоторые атрибуты или теги, которые могут вызвать XSS, DOMPurify вас не остановит:

const config = {
    ADD_TAGS: ["script"],
    ADD_ATTR: ["onclick"],
};

html = "abc<script>alert(1)</script><button onclick=alert(2)>abc</button>";
console.log(DOMPurify.sanitize(html, config)); // abc<script>alert(1)</script><button onclick="alert(2)">abc</button>

Правильная библиотека, неверное использование

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

Первый классический случай - это уязвимость, обнаруженная известным тайваньским хакером, апельсином, в 2019 году. При фильтрации контента HackMD использовал следующую конфигурацию (HackMD использует другой пакет под названием js-xss):

var filterXSSOptions = {
    allowCommentTag: true,
    whiteList: whiteList,
    escapeHtml: function (html) {
        // allow html comment in multiple lines
        return html
            .replace(/<(?!!--)/g, "&lt;")
            .replace(/-->/g, "-->")
            .replace(/>/g, "&gt;")
            .replace(/-->/g, "-->");
    },
    onIgnoreTag: function (tag, html, options) {
        // allow comment tag
        if (tag === "!--") {
            // do not filter its attributes
            return html;
        }
    },
    // ...
};

Если тег равен !--, он прямо игнорируется и не возвращается. Намерение было сохранить комментарии, например, <!-- Hello--> будет рассматриваться как тег с именем !--.

Однако, Orange обошел это следующим образом:

!-- foo="bar--><s>Hi</s>" -->

Поскольку <!-- рассматривается как тег, содержимое выше просто добавляет атрибут foo. Но когда браузер выполняет его рендеринг, открывающий <!-- сочетается с bar--> в foo, становясь комментарием HTML, и следующий <s>Hi</s> отображается, в результате возникает уязвимость XSS.

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

Веб-сайт на бэкенде санитизировал article.content, и frontend-rendering был написан следующим образом:

<>
  <div
    className={classNames({ "u-content": true, translating })}
    dangerouslySetInnerHTML={{
      __html: optimizeEmbed(translation || article.content),
    }}
    onClick={captureClicks}
    ref={contentContainer}
  />
  <style jsx>{styles}</style>
</>

Уже отфильтрованный контент прошел обработку optimizeEmbed, что означает, что если возникает проблема с optimizeEmbed, она все равно может вызвать XSS.

Давайте посмотрим, что делает эта функция (некоторый код отсутствует):

export const optimizeEmbed = (content: string) => {
    return content
        .replace(/\<iframe /g, '<iframe loading="lazy"')
        .replace(
            /<img\s[^>]*?src\s*=\s*['"]([^'"]*?)['"][^>]*?>/g,
            (match, src, offset) => {
                return /* html */ `
      <picture>
        <source
          type="image/webp"
          media="(min-width: 768px)"
          srcSet=${toSizedImageURL({ url: src, size: "1080w", ext: "webp" })}
          onerror="this.srcset='${src}'"
        />
        <img
          src=${src}
          srcSet=${toSizedImageURL({ url: src, size: "540w" })}
          loading="lazy"
        />
      </picture>
    `;
            }
        );
};

Здесь URL изображения напрямую соединяется в строку, и атрибуты не заключены в одиночные или двойные кавычки! Если мы можем контролировать toSizedImageURL, мы можем проэксплуатировать уязвимость XSS. Реализация этой функции следующая:

export const toSizedImageURL = ({ url, size, ext }: ToSizedImageURLProps) => {
    const assetDomain = process.env.NEXT_PUBLIC_ASSET_DOMAIN
        ? `https://${process.env.NEXT_PUBLIC_ASSET_DOMAIN}`
        : "";
    
    const isOutsideLink = url.indexOf(assetDomain) < 0;
    const isGIF = /gif/i.test(url);
    
    if (!assetDomain || isOutsideLink || isGIF) {
        return url;
    }
    
    const key = url.replace(assetDomain, ``);
    const extedUrl = changeExt({ key, ext });
    const prefix = size ? "/" + PROCESSED_PREFIX + "/" + size : "";
    
    return assetDomain + prefix + extedUrl;
};

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

<source 
    type="image/webp" 
    media="(min-width: 768px)" 
    srcSet="https://assets.matters.news/processed/1080w/embed/test" 
    style="animation-name: spinning;" 
    onanimationstart="console.log(1337);" 
    onerror="this.srcset='${src}'" 
/>

Используя style=animation-name:spinning вместе с обработчиком событий onanimationstart=console.log(1337), уязвимость XSS успешно создается без необходимости взаимодействия с пользователем.

Из двух представленных выше случаев мы видим, что: 1. Используется неправильная конфигурация 2. Изменение содержимого после фильтрации

Эти факторы могут вызвать проблемы и привести к уязвимостям XSS.

Таким образом, даже с правильной библиотекой, важно обращать внимание на способ ее использования. Малейшая ошибка все равно может привести к уязвимости XSS.

Заключение

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

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

Что касается backend, в PHP вы можете использовать функцию . В документации показано, какие символы она кодирует:

Однако, согласно спецификации - <!--> является допустимым пустым комментарием, поэтому вышеупомянутый код становится комментарием, за которым следуют тег <script> и текст -->.

- это открытый пакет, разработанный немецкой компанией по кибербезопасности Cure53, специально для санитизации HTML. В Cure53 много специалистов, которые специализируются на веб- и фронтенд-разработке и сообщили о многих известных уязвимостях. Они эксперты в этой области.

Документация DOMPurify достаточно подробна, и есть специальная страница под названием "", которая объясняет цели этой библиотеки и ситуации, в которых могут возникнуть проблемы.

Для получения более подробной информации и исправления, пожалуйста, обратитесь к оригинальной статье:

Если переданный URL равен style=animation-name:spinning onanimationstart=alert(1337), конкатенированный HTML будет:

Дополнительные ссылки: 1.

https://www.youtube.com/watch/../account
open redirect
https://www.youtube.com/redirect?target=https://blog.huli.tw
https://blog.huli.tw
htmlspecialchars
HTML5
DOMPurify
Цели безопасности и модель угроз
A Wormable XSS on HackMD
https://assets.matters.news/processed/1080w/embed/test
Preventing XSS may be more difficult than you think