- PVSM.RU - https://www.pvsm.ru -
Привет Хабрахабр,
В этой статье будут наглядно продемонстрирован результат применения некоторых важных и экзотичных HTTP заголовков, большинство из которых связаны с безопасностью.
Атака XSS (межсайтовый скриптинг) это тип атаки, при котором вредоносный код может быть внедрён в атакуемую страницу.
Например вот так:
<h1>Hello, <script>alert('hacked')</script></h1>
Такой тип атаки легко обнаружить и браузер вполне может с этим справиться: если в исходном коде есть нечто похожее на запросы, то это может оказаться угрозой.
И заголовок X-XSS-Protection управляет этим поведением браузера.
Принимаемые значения:
Создадим веб сервер-песочницу на node.js, чтобы посмотреть как это работает.
var express = require('express')
var app = express()
app.use((req, res) => {
if (req.query.xss) res.setHeader('X-XSS-Protection', req.query.xss)
res.send(`<h1>Hello, ${req.query.user || 'anonymous'}</h1>`)
})
app.listen(1234)
Буду использовать Google Chrome 55.
http://localhost:1234/?user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E
Ничего не произойдёт, браузер успешно заблокирует атаку. Chrome, по умолчанию, блокирует угрозу и сообщает об этом в консоли.
Он даже выделяет проблемный участок в исходном коде.
http://localhost:1234/?user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E&xss=0
О нет!
http://localhost:1234/?user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E&xss=1
Страница была очищена из-за явного указания заголовка.
http://localhost:1234/?user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E&xss=1;%20mode=block
В этом случае атака будет предотвращена путём блокирования загрузки страницы.
http://localhost:1234/?user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E&xss=1;%20report=http://localhost:1234/report
Атака предотвращена и сообщение об этом отправлено по соответствующему адресу.
При помощи данного заголовка можно защититься от так называемого Кликджекинга [Clickjacking].
Представьте, что у злоумышленника есть канал на YouTube и ему хочется больше подписчиков.
Он может создать страницу с кнопкой «Не нажимать», что будет значить, что все на неё обязательно нажмут. Но поверх кнопки находится абсолютно прозрачный iframe и в этом фрейме прячется страница канала с кнопкой подписки. Поэтому при нажатии на кнопку, на самом деле пользователь подписывается на канал, если конечно, он был залогинен в YouTube.
Продемонстрируем это.
Сперва нужно установить расширение [1] для игнорирования данного заголовка.
Создадим простую страницу.
<style>
button { background: red; color: white; padding: 10px 20px; border: none; cursor: pointer; }
iframe { opacity: 0.8; z-index: 1; position: absolute; top: -570px; left: -80px; width: 500px; height: 650px; }
</style>
<button>Do not click his button!</button>
<iframe src="https://youtu.be/dQw4w9WgXcQ?t=3m33s"></iframe>
Как можно заметить, я разместил фрейм с подпиской прям над кнопкой (z-index: 1) и поэтому если попытаться на неё нажать, то на самом деле нажмётся фрейм. В этом примере фрейм не полностью прозрачен, но это исправляется значением opacity: 0.
На практике, такое не сработает, потому что у YouTube задан нужный заголовок, но смысл угрозы, надеюсь, понятен.
Для предотвращения страницы быть использованной во фрейме нужно использовать заголовок X-Frame-Options.
Принимаемые значения:
Нам понадобится веб сервер для демонстрации
var express = require('express')
for (let port of [1234, 4321]) {
var app = express()
app.use('/iframe', (req, res) => res.send(`<h1>iframe</h1><iframe src="//localhost:1234?h=${req.query.h || ''}"></iframe>`))
app.use((req, res) => {
if (req.query.h) res.setHeader('X-Frame-Options', req.query.h)
res.send('<h1>Website</h1>')
})
app.listen(port)
}
Все смогут встроить наш сайт по адресу localhost:1234 во фрейм.
Страницу вообще нельзя использовать во фрейме.
Только страницы с одинаковым источником смогут встраивать во фрейм. Источники совпадают, если домен, порт и протокол одинаковые.
Похоже, что Chrome игнорирует такую опцию, т.к. существует заголовок Content-Security-Policy (о ней будет рассказано ниже). Не работает это и в Microsoft Edge.
Ниже Mozilla Firefox.
Данный заголовок предотвращает атаки с подменой типов MIME (<script src=«script.txt»>) или несанкционированного хотлинка (<script src=«https://raw.githubusercontent.com/user/repo/branch/file.js»>)
var express = require('express')
var app = express()
app.use('/script.txt', (req, res) => {
if (req.query.h) res.header('X-Content-Type-Options', req.query.h)
res.header('content-type', 'text/plain')
res.send('alert("hacked")')
})
app.use((req, res) => {
res.send(`<h1>Website</h1><script src="/script.txt?h=${req.query.h || ''}"></script>`)
})
app.listen(1234)
http://localhost:1234/
Хоть script.txt и является текстовым файлом с типом text/plain, он будет запущен как скрипт.
http://localhost:1234/?h=nosniff
На этот раз типы не совпадают и файл не будет исполнен.
Это относительно молодой заголовок и помогает уменьшить риски атаки XSS в современных браузерах путём указания в заголовке какие именно ресурсы могут подргружаться на странице.
Например, можно попросить браузер не исполнять inline-скрпиты и загружать файлы только с одного домена. Inline-скрпиты могут выглядеть не только как <script>...</script>, но и как <h1 onclick="...">.
Посмотрим как это работает.
var request = require('request')
var express = require('express')
for (let port of [1234, 4321]) {
var app = express()
app.use('/script.js', (req, res) => {
res.send(`document.querySelector('#${req.query.id}').innerHTML = 'изменено ${req.query.id}-скриптом'`)
})
app.use((req, res) => {
var csp = req.query.csp
if (csp) res.header('Content-Security-Policy', csp)
res.send(`
<html>
<body>
<h1>Hello, ${req.query.user || 'anonymous'}</h1>
<p id="inline">это будет изменено inline-скриптом?</p>
<p id="origin">это будет изменено origin-скриптом?</p>
<p id="remote">это будет изменено remote-скриптом?</p>
<script>document.querySelector('#inline').innerHTML = 'изменено inline-скриптом'</script>
<script src="/script.js?id=origin"></script>
<script src="//localhost:1234/script.js?id=remote"></script>
</body>
</html>
`)
})
app.listen(port)
}
Это работает так, как вы и ожидали
http://localhost:4321/?csp=default-src%20%27none%27&user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E
default-src применяет правило для всех ресурсов (картинки, скрипты, фреймы и т.д.), значение 'none' блокирует всё. Ниже продемонстрировано что происходит и ошибки, показываемые в браузере.
Chrome отказался запускать любые скрипты. В таком случае не получится даже загрузить favicon.ico.
http://localhost:4321/?csp=default-src%20%27self%27&user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E
Теперь можно использовать ресурсы с одного источника, но по прежнему нельзя запускать внешние и inline-скрипты.
http://localhost:4321/?csp=default-src%20%27self%27;%20script-src%20%27self%27%20%27unsafe-inline%27&user=%3Cscript%3Ealert(%27hacked%27)%3C/script%3E
На этот раз мы разрешили исполнение и inline-скриптов. Обратите внимание, что XSS атака в запросе тоже была заблокирована. Но этого не произойдёт, если одновременно поставить и unsafe-inline, и X-XSS-Protection: 0.
На сайте content-security-policy.com [3] красиво показаны множество примеров.
Я этого не проверял, но я думаю, что следующие заголовки эквиваленты:
Если взглянуть на заголовки facebook.com или twitter.com, то можно заметить, что эти сайты используют много CSP.
HTTP Strict Transport Security (HSTS) это механизм политики безопасности, который позволяет защитить сайт от попытки небезопасного соединения.
Допустим, что мы хотим подключиться к facebook.com. Если не набрать перед запросом https://, то протокол, по умолчанию, будет выбран HTTP и поэтому запрос будет выглядеть как http://facebook.com.
$ curl -I facebook.com
HTTP/1.1 301 Moved Permanently
Location: https://facebook.com/
После этого мы будем перенаправлены на защищённую версию Facebook.
Если подключиться к публичной WiFi точке, которая принадлежит злоумышленнику, то запрос может быть перехвачен и вместо facebook.com злоумышленник может подставить похожую страницу, чтобы узнать логин и пароль.
Чтобы обезопаситься от такой атаки, можно использовать вышеупомянутый заголовок, который скажет клиенту в следующий раз использовать https версию сайта.
$ curl -I https://www.facebook.com/
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=15552000; preload
Если пользователь был залогинен в Facebook дома, а потом попытался открыть его из небезопасной точки доступа, то ему ничего не угрожает, т.к. браузеры запоминают этот заголовок.
Но что будет, если подключиться в небезопасной сети первый раз? В этом случае защититься не получится.
Но у браузеров есть козырь и на этот случай. В них есть предопределённый список доменов, для которых следует использовать только HTTPS.
Можно отправить свой домен по этому [4] адресу. Там также можно узнать правильно ли используется заголовок.
Принимаемые значения:
А если потребуется переключиться на HTTP перед сроком истечения max-age или если установлен preload? Не получится. Этот заголовок требует строгого соблюдения. Поэтому в этом случае пользователю придётся очистить историю и настройки.
HTTP Public Key Pinning (HPKP) это механизм политики безопасности, который позволяет HTTPS сайтам защититься от использования злоумышленниками поддельных или обманных сертификатов.
Принимаемые значения:
Вместо заголовка Public-Key-Pins можно использовать Public-Key-Pins-Report-Only, в таком случае будут отправляться только сообщения об ошибках совпадения ключей, но браузер всё равно будет загружать страницу.
Так делает Facebook:
$ curl -I https://www.facebook.com/
HTTP/1.1 200 OK
...
Public-Key-Pins-Report-Only:
max-age=500;
pin-sha256="WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=";
pin-sha256="r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=";
pin-sha256="q4PO2G2cbkZhZ82+JgmRUyGMoAeozA+BSXVXQWB8XWQ=";
report-uri="http://reports.fb.com/hpkp/"
Зачем это нужно? Не достаточно ли доверенных центров сертификации (CA)?
Злоумышленник может создать свой сертификат для facebook.com и путём обмана заставить пользователя добавить его в своё хранилище доверенных сертификатов, либо он может быть администратором.
Попробуем создать сертификат для facebook.
sudo mkdir /etc/certs
echo -e 'USnCAnSFnFBnXXnwww.facebook.comnno@spam.org' |
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout /etc/certs/facebook.key
-out /etc/certs/facebook.crt
И сделать его доверенным в локальной системе.
# curl
sudo cp /etc/certs/*.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
# Google Chrome
sudo apt install libnss3-tools -y
certutil -A -t "C,," -n "FB" -d sql:$HOME/.pki/nssdb -i /etc/certs/facebook.crt
# Mozilla Firefox
#certutil -A -t "CP,," -n "FB" -d sql:`ls -1d $HOME/.mozilla/firefox/*.default | head -n 1` -i /etc/certs/facebook.crt
А теперь запустим веб сервер, использующий этот сертификат.
var fs = require('fs')
var https = require('https')
var express = require('express')
var options = {
key: fs.readFileSync(`/etc/certs/${process.argv[2]}.key`),
cert: fs.readFileSync(`/etc/certs/${process.argv[2]}.crt`)
}
var app = express()
app.use((req, res) => res.send(`<h1>hacked</h1>`))
https.createServer(options, app).listen(443)
Переключимся на сервер
echo 127.0.0.1 www.facebook.com | sudo tee -a /etc/hosts
sudo node server.js facebook
Посмотрим что получилось
$ curl https://www.facebook.com
<h1>hacked</h1>
Отлично. curl подтверждает сертификат.
Так как я уже заходил на Facebook и Google Chrome видел его заголовки, то он должен сообщить об атаке но разрешить страницу, так?
Неа. Ключи не проверялись из-за локального корневого сертификата [Public-key pinning bypassed]. Это интересно…
Хорошо, а что насчёт www.google.com [5]?
echo -e 'USnCAnSFnGooglenXXnwww.google.comnno@spam.org' |
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048
-keyout /etc/certs/google.key
-out /etc/certs/google.crt
sudo cp /etc/certs/*.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
certutil -A -t "C,," -n "Google" -d sql:$HOME/.pki/nssdb -i /etc/certs/google.crt
echo 127.0.0.1 www.google.com | sudo tee -a /etc/hosts
sudo node server.js google
Тот же результат. Думаю это фича.
Но в любом случае, если не добавлять эти сертификаты в локальное хранилище, открыть сайты не получится, потому что опции продолжить небезопасное соединение в Chrome или добавить исключение в Firefox не будет.
Данные сжаты при помощи Brotli [6].
Алгоритм обещает лучшее сжатие чем gzip и сравнимую скорость разархивирования. Поддерживается Google Chrome [7].
Разумеется, для него есть модуль в node.js.
var shrinkRay = require('shrink-ray')
var request = require('request')
var express = require('express')
request('https://www.gutenberg.org/files/1342/1342-0.txt', (err, res, text) => {
if (err) throw new Error(err)
var app = express()
app.use(shrinkRay())
app.use((req, res) => res.header('content-type', 'text/plain').send(text))
app.listen(1234)
})
Исходный размер: 700 Кб
Brotli: 204 Кб
Gzip: 241 Кб
С помощью Resource Timing API [8] можно узнать сколько времени заняла обработка ресурсов на странице.
Поскольку информация о времени загрузки может быть использована чтобы определить посещал ли пользователь страницу до этого (обращая внимание на то, что ресурсы могут кэшироваться), стандарт считается уязвимым, если давать такую информацию любым хостам.
<script>
setTimeout(function() {
console.log(window.performance.getEntriesByType('resource'))
}, 1000)
</script>
<img src="http://placehold.it/350x150">
<img src="/local.gif">
Похоже, если не указать Timing-Allow-Origin, то получить детальную информацию о времени операций (поиска домена, например) можно только для ресурсов с одним источником.
Использовать можно так:
Альтернативные Сервисы [Alternative Services] позволяют ресурсам находиться в различных частях сети и доступ к ним можно получить с помощью разных конфигураций протокола.
Такой используется в Google:
Это означает, что браузер, если захочет, может использовать QUIC [9], это HTTP над UDP, через порт 443 следующие 30 дней (ma = 2592000 секунд, или 720 часов, т.е 30 дней). Понятия не имею что означает параметр v, версия?
Ниже несколько P3P заголовков, которые я встречал:
Некоторые браузеры требуют, чтобы cookies третьих лиц поддерживали протокол P3P для обозначения мер конфиденциальности.
Организация, основавшая P3P, Консорциум Всемирной паутины (W3C), приостановила работу над протоколом несколько лет назад из-за того, что современные браузеры не до конца поддерживают протокол. В результате, P3P устарел и не включает в себя технологии, которые сейчас используются в сети, поэтому большинство сайтов не поддерживают P3P.
Я не стал слишком углубляться, но видимо заголовок нужен для IE8 чтобы принимать cookies третьих лиц.
Например, если в IE настройка приватности высокая, то все cookies с сайтов, у которых нет компактной политики конфиденциальности, будут блокированы, но те у которых есть заголовки похожие на вышеупомянутые, заблокированы не будут.
Автор: A3a
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/bezopasnost/221490
Ссылки в тексте:
[1] расширение: https://chrome.google.com/webstore/detail/ignore-x-frame-headers/gleekbfjekiniecknbkamfmkohkpodhe/
[2] localhost: http://localhost
[3] content-security-policy.com: https://content-security-policy.com/
[4] этому: https://hstspreload.appspot.com/
[5] www.google.com: http://www.google.com
[6] Brotli: https://github.com/google/brotli
[7] Поддерживается Google Chrome: https://www.chromestatus.com/feature/5420797577396224
[8] Resource Timing API: https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API
[9] QUIC: https://ma.ttias.be/googles-quic-protocol-moving-web-tcp-udp/
[10] support.google.com/accounts/answer/151657?hl=en: https://support.google.com/accounts/answer/151657?hl=en
[11] fb.me/p3p: http://fb.me/p3p
[12] Источник: https://habrahabr.ru/post/317720/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.