Уязвимости на странице — Введение в тестирование веб-приложений
Представим, что мы сидим в чате и во время разговора сайт просит нас еще раз авторизоваться — ввести логин и пароль. Возможно, случился сбой, поэтому мы вводим свои данные. Но в итоге сообщение пропадает, и через пару минут нас выкидывает из чата, а пароль больше не подходит.
Любые проекты, чаты и сайты пишут люди. Чем крупнее проект, тем больше шансов, что у сайта или приложения найдутся так называемые «дыры» — незащищенные участки, которыми могут воспользоваться злоумышленники.
В этом уроке мы познакомимся с понятием уязвимости и разберем одну из самых простых в исполнении атак — XSS
Уязвимости и атаки
Цели злоумышленников при атаках на сайты или приложения обычно следующие:
- Кража персональных данных пользователей
- Кража аккаунтов
- Кража банковских данных
- Передача вирусов на компьютеры пользователей
Чтобы преступники не достигли их, на помощь приходят специалисты, которые занимаются «этичным хакингом». Их называют пентестерами — от английского PenTester, или специалистами по проникновению. Их задача — проанализировать сайт или приложения, найти незащищенные места и определить, как этим может воспользоваться злоумышленник.
Также предотвратить атаки могут тестировщики, хоть они и не являются специалистами по информационной безопасности. Но они могут проверить сайт на самые базовые и распространенные виды атак. Это не закроет все проблемы с безопасностью, но простые способы взлома можно исправить без привлечения специалистов.
Существует два самых распространенных типа атак:
- Cross-site Script (XSS)
- SQL Injection
В дополнительных материалах к этому уроку будет приложена ссылка на список ТОП-10 атак в интернете по версии OWASP — некоммерческой организации, которая исследует информационную безопасность и виды атак на ресурсы.
В примере про чат злоумышленник смог отправить под видом сообщения HTML-разметку с формой для авторизации. В этой ситуации слабым звеном оказалось окно с выводом сообщения. Разработчики не предусмотрели, что можно отправить любую HTML-разметку вместо сообщения, и она будет выведена на экране.
В этом случае говорят, что злоумышленник нашел уязвимость и использовал ее. Уязвимость — это недостаток в системе, с помощью которого можно нарушить работу всей системы или отдельной ее части. Одна из таких уязвимостей связана с возможностью отправки кода через формы на сайте или в приложении. Атака, которая использует эту уязвимость, называется XSS. Разберем ее подробнее.
XSS
Когда злоумышленник отправил вместо обычного сообщения разметку на HTML, он использовал атаку Cross-Site Scripting или XSS. Сокращение начинается с буквы «X», чтобы не было путаницы с языком стилей CSS. При XSS должен выполниться любой сторонний код, который был передан на сайт. Это может быть:
- HTML-разметка
- JavaScript код
То есть код, который может выполниться в браузере любого пользователя. Такому типу атак подвержены все элементы на странице, в которые можно ввести текст, например, поля форм.
Для примера создадим такой сайт и отправим код в незащищенное поле. Код — обычный JavaScript код, в котором выводится сообщение для пользователя в отдельном окне:
script>alert('Я отправил вам скрипт! Пришлите денег, пожалуйста :(')/script>
Так выглядит страница до ввода сообщения со скриптом:
При вводе сообщения со скриптом браузеры всех пользователей автоматически выполнят скрипт, так как в чате нет защиты от XSS:
Это происходит, потому что браузер обрабатывает все символы в сообщении, в том числе и служебные, такие как < , >. Чтобы безопасно вывести сообщение и не выполнить код, используется замена символов на мнемоники.
Мнемоники — специальные символы, которые браузер не обрабатывает как код. Такой процесс называется экранированием символов и повсеместно используется в программировании.
В примере выше, чтобы защититься от выполнения кода, стоит заменить все символы < на мнемонику < , а символы >на > . Браузеры не умеют обрабатывать такой текст как код, но для пользователя все символы вернутся в исходное состояние.
<script>alert('Я отправил вам скрипт! Пришлите денег, пожалуйста :(')</script>
Для тестирования XSS используются два способа:
- Если переданная строка в форме где-то отображается после отправки, то возможно отправить любой HTML-тег с текстом. Например,
Hello
- Вместо HTML воспользуйтесь кодом на JavaScript, как в примере выше. Действие функции alert() будет заметно сразу
Необходимо взять примеры из этого урока и вставить их в поля формы тестируемого сайта. Будет нагляднее, если результат выводится сразу, например, как у формы комментариев. Также нужно представить себя злоумышленником, который ищет уязвимость в проекте.
Если ни один из способов не сработал, значит, простыми способами атаку не использовать. Это не означает, что уязвимостей нет, но так отсекаются самые распространенные способы, которыми любят пользоваться начинающие хакеры.
Чтобы проверить сайт на XSS, необходимо проанализировать все поля, в которых пользователь может оставлять текст. В большинстве случаев это происходит в формах, поэтому проверка на XSS происходит во время тестирования форм.
При использовании XSS атака происходит на пользователей, а не на сам сайт и его внутреннюю структуру. В противовес такому подходу используют атаку под названием SQL Injection. Она направлена на базу данных сайта.
Выводы
В этом уроке мы базово рассмотрели тему информационной безопасности и простых атак на веб-приложения. Так как тема безопасности большая для одного урока, то мы остановились на одной из простых в исполнении атаки — Cross-site Script.
XSS простая, но ее последствия не настолько безобидны. С помощью XSS можно получить данные о пользователе или подсунуть ему поддельную форму регистрации.
Суть уязвимости состоит в не обработке полей формы, поиска, комментариев. Если разработчики не экранируют символы в таких полях, то появится возможность отправить и выполнить вредоносный код на сайте или в приложении
С опытом вы сможете тестировать большое количество разных атак и дополнительно защищать приложение от злоумышленников.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Об обучении на Хекслете
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Урок «Как эффективно учиться на Хекслете»
- Вебинар « Как самостоятельно учиться »
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Анализ уязвимостей процесса аутентификации
Последней каплей вдохновения для этого поста стал перевод курса MIT «Безопасность компьютерных систем» от @ua-hosting, огромное спасибо им за это. Если кто-то еще не читал/смотрел этот курс, то я настоятельно его рекомендую.
А для затравки и в качестве введения для этого поста приведу примеры, которые приводились во введение в этом курсе.
В первую очередь хочется сказать, что материал который предоставляется в этом посте нельзя использовать в незаконных целях и направлен на то, чтобы мы как разработчики знали как может вести себя злоумышленник и были защищены от этого, а не наоборот. Если вы захотите попробовать некоторые из подходов на практике, то прочитайте вот эту статью «Ответы юриста: как избежать ответственности за поиск уязвимостей».
В определенный момент времени Yahoo решила дать своим пользователям получать доступ к аккаунту не только по логину и паролю, но и в случае, если вы забыли пароль, ответить на пару вопросов, ответ на который могли знать только вы (т.к. у них не было возможности отправить вам пароль на какой либо другой резервный аккаунт).
И в один прекрасный день, Сара Пэйлин, у которой был ящик на Yahoo, обнаружила, что данные ее почтового ящика утекли. Дело в том, что на кодовые вопросы для доступа к аккаунту были «Где вы посещали школу? Как звали вашего друга? Когда у вас день рождения?», а ответы на эти вопросы были на ее странице в википедии. И каждый мог получить доступ к ее аккаунту, просто прочитав страницу о ней.
Еще один «прекрасный» случай произошел с парнем по имени Мэт Хонан. Злоумышленников заинтересовал твиттер этого человека. Из персонального сайта нашли его email — mhonan@gmail.com, а информация Whois одного из проектов говорила что его адрес 1559B Sloat Blvd, San Francisco. Далее злоумышленник через форму восстановления пароля google узнал его резервный ящик m••••n@me.com. Ящики на me.com — это по совместительству еще и AppleID — идентификатор, используемый для совершения покупок в магазине AppStore. Дабы восстановить пароль к аккаунту @me.com через звонок в техподдержку Apple, необходимо знать:
- адрес почты @me.com;
- billing address — адрес проживания;
- последние 4 цифры кредитной карты, привязанной к AppleID.
Первые два пункта хакер уже знал. Осталось узнать последние 4ре цифры кредитки, и в этом ему помог Amazon. Хакер позвонил в техподдержку Амазона, представился Мэттом и сказал, что хочет привязать к аккаунту еще одну кредитную карту. Все что нужно знать для этого — ФИО, мыло и адрес. Далее он повесил трубку и позвонил еще раз, но уже с другим запросом — он «забыл пароль от аккаунта» и хотел бы привязать еще один ящик для восстановления пароля. Всё, что нужно знать в таком случае — ФИО, адрес и номер свежей добавленной кредитной карты (!). Теперь осталось зайти на страничку Амазона и посмотреть последние 4 цифры привязанной старым хозяином кредитной карты. Вуаля! Подробнее об этом случае можно почитать здесь.
Какие выводы мы можем сделать по этим примерам:
- Взлом подразумевает наличие “плохого парня”
- Подход к ИБ должен быть глобальным
- Чем меньше мы открываем данных, тем безопаснее система.
Но давайте продолжим и рассмотрим технические детали и возможные уязвимости одного из основополагающих процессов — процесса аутентификации.
Так же очень хочется уделить особое внимание инструментам, которые помогут нам не быть слепыми котятами и дать возможность более прозрачно видеть детали процесса отправки запросов. В частности, изначально пост назывался «Анализ уязвимостей форм для аутентификации», но я из него убрал слово «форм», так как хочется показать такой инструмент, как postman.
- Postman
- tcpdump
- wireshark
- katools (kali linux tool)
- puppeteer
Давайте рассмотрим очень простой пример.
Очень простой пример
Задача: нам нужно по логину и паролю аутентифицировать пользователя.
Для этого создадим табличку:
CREATE TABLE users ( login TEXT NOT NULL UNIQUE, password TEXT NOT NULL );
Как БД будем использовать sqlite3 .
И напишем запрос аутентификации:
app.get('/api/v1/login', (req, res) => db.all(` SELECT rowid AS id, login, password FROM users WHERE login = '$req.query.login>' `, function(err, rows) if (err) res.send(err.message); > else let loginFlag = false; if (rows && rows.length > 0) if (req.query.password === rows[0].password) loginFlag = true; > > res.send(loginFlag ? "logged in" : "bad news"); > >) >)
Полный листинг сервера здесь.
UPD: Никогда! Никогда! НИКОГДА! не выводите “голые” ошибки и стэктрейсы в ответ. Так как это даст дополнителную информацию злоумышленнику. Например, в нашем примере res.send(err.message); даст информацию о том, что мы используем SQLite.
Что здесь происходит, по GET запросу, мы идем в БД и ищем запись по логину. Если такая есть, сверяем пароль который к нам пришел с тем, который записан в БД. Если все ок выводим «logged in» , если нет «bad news» . В базе у нас заведен пользователь с логин = «v1» и паролем «123456».
Этот код, является отличным рассадником уязвимостей, давайте посмотрим почему.
Во-первых. Давайте воспользуемся Postman и составим запрос: 127.0.0.1:3030/api/v1/login?login=v1&password=123456 .
В ответ, мы получим «logged in» , если мы введем другие данные, например 127.0.0.1:3030/api/v1/login?login=whatthefoxsay&password=test , то получим «bad news» , т.е. API работает и выполняет свою задачу.
Но что будет, если мы отправим: 127.0.0.1:3030/api/v1/login?login=whatthefoxsay’&password=test
На придет SQLITE_ERROR: unrecognized token: «‘whatthefoxsay»»
Происходит это потому, что мы используем вот такую конструкцию:
login = '$'
И в результате формирования запроса мы получим:
SELECT rowid AS id, login, password FROM users WHERE login = 'whatthefoxsay'';
Где есть синтаксическая ошибка в виде последнего знака ‘ .
Ок. получив эту ошибку, мы можем догадаться что при обращении /api/v1/login идет запрос в БД с поиском логина и пароля. Значит мы можем попробовать каким-то образом немного изменить запрос и подставить туда свои данные.
в SQL есть оператор UNION , который позволяет объединять запрос в один по строкам. Работает он так:
SELECT 1,2,3 UNION SELECT 3,2,1;
1,2,3 3,2,1
Давайте попробуем им воспользоваться:
/login?login=whatthefoxsay' union select 'test', 'test&password=test
SQLITE_ERROR: SELECTs to the left and right of UNION do not have the same number of result columns
ага, в данном ответе нам говорят что количество колонок не совпадает. Ок. Давайте это исправим:
/login?login=whatthefoxsay' union select 'test', 'test', 'test&password=test
Получаем: logged in
Вуаля! мы прошли аутентификацию.
Как нам обезопаситься от таких случаев? Все драйвера для БД должны предоставлять инструменты для экранирования входных параметров. В примере с sqlite3 нам нужно использовать вместо:
conn.all(` SELECT rowid AS id, login, password FROM users WHERE login = '$req.query.login>' `, function (err, rows) > );
conn.all(` SELECT rowid AS id, login, password FROM users WHERE login = $login `, $login: req.body.login >, function (err, rows) > );
Полный листинг сервера здесь.
В этом случае символ ‘ будет экранироваться и мы получим что-то типа: login = ‘whatthefoxsay\’ union select \’test\’, \’test\’, \’test’
Что нас спасет и при попытке отправить:
/login?login=whatthefoxsay' union select 'test', 'test', 'test&password=test
Получим: bad news
Драйвера всегда предоставляют возможность безопасной интерполяции параметров. Используете это!
Например в документации psycopg2 (python драйвер для работы с postgresql), написано:
Warning: Never, never, NEVER use Python string concatenation (+) or string parameters interpolation (%) to pass variables to a SQL query string. Not even at gunpoint.
Предупреждение: Никогда, никогда, НИКОГДА не используйте конкатенацию строк (+) или (%) интерполяцию, чтобы передать параметры в SQL запрос. Даже под дулом пистолета.
Разобрались. Давайте пойдем дальше, и заметим, что мы отсылаем GET запрос с нашим логином и паролем. Мало того! Мы используем http вместо https ! Нужно помнить о том, что URL запроса (часть которого — это GET параметры)гораздо чаще логируются HTTP серверами и если у злоумышленника будет доступ к логам сервера, он сможет получить их.
TCP/HTTP(s) GET vs POST
в случае, с HTTP , что с POST , что c GET возможна очень простая атака посредника (Man in the middle (MITM)), если конечно у нас есть возможность оказаться посередине. Как пример, вот вы пришли в кафе, подключились к wifi. А какой-то плохой парень взломал роутер и решил записать весь трафик проходящий через него, чтобы узнать куда ходят посетители кафе.
В этом случае он на одном из узлов записывает tcp-дамп:
sudo tcpdump -i lo port 3030 -w ./dump.pcap
- -i lo — сетевой интерфейс который мы слушаем, так как у меня все развернуто локально я слушаю localhost .
- port 3030 — чтобы не засорять эфир, ограничимся портом 3030, который слушает наше приложение.
- -w ./dump.pcap — говорит писать дамп в файл.
После отправим два запроса и GET , и POST на наше API аутентификации /api/v1/login . По завершению записи мы воспользуемся wireshark , чтобы посмотреть что получилось. И увидим, что в дампе оказались наши два запроса с явками и паролями.
Давайте подключим HTTPS и перейдем на POST
Для начала создадим самоподписанные сертификаты:
openssl req -nodes -new -x509 -keyout server.key -out server.cert
Теперь подключим их к нашему приложению:
const express = require('express'); const app = express(); const bodyParser = require('body-parser'); app.use(bodyParser.json()); const fs = require('fs'); const https = require('https'); const privateKey = fs.readFileSync('./server.key', 'utf8'); const certificate = fs.readFileSync('./server.cert', 'utf8'); const credentials = key: privateKey, cert: certificate>; app.post('/api/v1/login', (req, res) => /* здесь без изменений */ >) const httpsServer = https.createServer(credentials, app); httpsServer.listen(3030);
Полный листинг сервера здесь
Если мы посмотрим на HTTPS трафик, то здесь будет все хорошо, так как SSL/TLS шифрует данные «между» TCP и HTTP протоколами (см. картинку ниже), т.е. все данные HTTP-запроса будут зашифрованы. Кстати, с HTTPS тоже могут быть проблемы, если сотворить определенную магию с сертификатами, но мы этой темы касаться не будем, скажу одно использовать самоподписанные сертификаты в большинстве случаев является плохой практикой.
А вот что видно в дампе:
Если отойти от аутентификации, архитектурный стиль REST API для получения данных рекомендует именно GET . Но давайте представим, что вы пишете приложение для мед.страховой, где есть API /users , которое принимает параметры: Имя, фамилия, отчество, дата рождения и номер полиса ДМС. В таком случае наш запрос должен выглядеть примерно так: /users?name=Vadim&last_name=Gorbachev&polic=123456 . Соответственно эта информация осядет в логах HTTP сервера, который пример запрос, и эти в них злоумышленник сможет получить пароль в незашифрованном виде. Ах да! шифрование пароля! Это следующая часть нашего доклада.
Шифрование (crypto vs bcrypt)
давайте зашифруем наши пароли, чтобы не хранить в БД в открытом виде, для этого воспользуемся модулем crypto :
crypto.createHash('md5').update(password).digest('hex');
Полный листинг сервера здесь.
Окей, кажется теперь получше, да? Не совсем.
Во-первых, не используете MD5 , почему спросите вы? А потому, что команда:
crypto.createHash('md5').update('123456').digest('hex');
сгенерирует нам хэш: e10adc3949ba59abbe56e057f20f883e
И если мы воспользуемся таким инструментом как google, то получим: https://google.gik-team.com/?q=e10adc3949ba59abbe56e057f20f883e что ваш хэш уже даже проиндексирован в поисковике.
Этот пример, конечно утрированный, если мы введем пароль, к примеру whatthefoxsay , его будет сложнее найти, так как он не настолько простой как 123456 . Но тем, не менее взломать его возможно. И вообще, криптография нам дает только время. Т.е. любой хэш со временем можно взломать. Важно понимать что время может быть равно 10 секундам и 1 млн. лет. Поэтому чем более криптостойкие алгоритмы вы используете, тем надежнее у вас защита (но помните, что она никогда не будет равна 100%, хотя при хороших условиях будет к этому значению стремиться).
Кстати, хотелось бы упомянуть такой инструмент как katools который является сборником инструментов для анализа уязвимостей репозитория Kali Linux . в его состав входит такая вещь как findmyhash . И с помощью команды
findmyhash MD5 -h e10adc3949ba59abbe56e057f20f883e
мы cможем опросить популярные источники «не знаю ли они такого хэша?».
***** HASH CRACKED!! ***** The original string is: 123456 The following hashes were cracked: ---------------------------------- e10adc3949ba59abbe56e057f20f883e -> 123456
Крайне не рекомендую вводить в подобные инструменты боевые хэши которые вы используете на проде.
MD5 плох! Поэтому давайте воспользуемся, чем-то более надежным.
const getFuncSHA512Salt = (salt) => return (password) => var hash = crypto.createHmac('sha512', salt); hash.update(password); var value = hash.digest('hex'); return value; > >; const cryptoSHA512Salt = getFuncSHA512Salt("HVHSNrRWpP1ZSR4bnjXpiHCS1ENYcUuHO")
Хотел отметить, что здесь мы добавляем соль HVHSNrRWpP1ZSR4bnjXpiHCS1ENYcUuHO . Такое использование, это минимальный способ шифрования который стоит использовать в наше время (возможно, будет не так страшно использова sha256 , но все же).
const secret = 'abcdefg'; const hash = crypto.createHmac('sha256', secret) .update('I love cupcakes') .digest('hex');
Они по умолчанию приводят пример хеширования уже с солью. Спасибо им!
Давайте немного отвлечемся и рассмотрим еще одну cool story. https://www.opennet.ru/opennews/art.shtml?num=46768
Возможно, вы помните как npm отозвал пароли около 170 тыс. аккаунтов и вот этот пост. А все дело в том, что очень интересный человек Сковорода Никита Андреевич @ChALkeR, который сейчас состоит в рабочей группе по безопасности node.js, провел анализ уязвимости аутентификации пользователей в npm.
возможно вы использовали пакеты из списка: Express, EventEmitter2, mime-types, semver, npm, fstream, cookie (и cookies), Bower, Component, Connect, koa, co, tar, css, gm, csrf, keygrip, jcarousel, serialport, basic-auth, lru-cache, inflight, mochify, denodeify, и многие другие.
К которым Никита получил доступ и описал подробности в статье опубликованной 2015-12-04.
Позже он провел еще одно исследование от опубликованное 2017-06-21
В котором рассказал что ситуация слабо изменилась. В этот раз список ТОП пакетов был такой: debug, qs, supports-color, yargs, commander, request, strip-ansi, chalk, form-data, mime, tunnel-agent, extend, delayed-stream, combined-stream, forever-agent, concat-stream, vinyl, co, express, escape-html, path-to-regexp, component-emitter, moment, ws, handlebars, connect, escodegen, got, gulp-util, ultron, http-proxy, dom-serializer, url-parse, vinyl-fs, configstore, coa, csso, formidable, color, winston, node-sass, react, react-dom, rx, postcss-calc, superagent, basic-auth, cheerio, jsdom, gulp, sinon, useragent, deprecated, browserify, redux, array-equal, bower, jshint, jasmine, global, mongoose, vhost, imagemin, highlight.js, tape, mysql, mz, nock, rollup, gulp-less, rework, xcode, ionic, cordova, normalize.css, electron, n, react-native, ember-cli, yeoman-generator, nunjucks, koa, modernizr, yo, mongoskin, и многие другие.
Результаты оказались ошеломляющими:
- из 126 тыс, удалось получить доступ к 17 тыс, что примерно 13%
- Количество пострадавших пакетов 73983 — 14% экосистемы.
- Количество пострадавших через зависимости — 54%
- Получил аккаунты 4 пользователей из списка топ-20
- 42 пользователя имели более 10 миллионов загрузок в месяц (каждый).
- 13 пользователей имели более 50 миллионов.
- Одним из аккаунтов с доступом к koa был «password»
- 662 — «123456», 174 — «123», 124 — password».
- 11% пользователей повторно использовали свои просочившиеся пароли: 10,6% — напрямую, и 0,7% — с очень незначительными изменениями.
- т.е. он мог бы контролировать 1 972 421 945 загрузок в месяц (напрямую), это 20% от общего числа.
Заметьте 662 пользователей использовали пароль 123456 . А вы говорите, я утрирую =)
Один из кейсов получения паролей, был использование базы утекших паролей с других ресурсов. Например, вы зарегистрировались с паролем whatthefoxsay в npm и на сайте blabla.site. Если взломают сайт blabla.site и получат все явки из него. Как вы думаете злоумышленники не захотят пройтись по ТОП-20 сайтам (таких как gmail, facebook, twitter, . npm, github) с попытками ввода логинов/паролей из базы? Чтобы проверить есть ли у вас утекшие логины, можно воспользоваться https://haveibeenpwned.com/ , который рекомендует npm. Будете ли вы получать спам на почту после того как введете вашу почту? Я не знаю, но если вы доверяете npm и их рекомендациям, то можете проверить =)
Возможно, хороший способ обезопасить себя от подобных ситуаций является использование менеджеров паролей. Можно ознакомиться с топом в этой статье, многие рекомендуют 1password . Но давайте не будет на этом задерживаться и пойдем дальше.
crypto vs bcrypt
Давайте, обратимся к гуру интернетов чтобы узнать действительно ли мы делаем все правильно. И по запросу node.js best practice наткнемся на репозиторий, который действительно содержит очень много нужных и полезных рекомендаций + к этим рекомендациям многие прислушиваются, о чем свидетельствуют звездочки на github, которых около 23 тыс. В нем есть раздел 6-security-best-practices , в котором даются советы и на часть примеров которые мы рассмотрели и на многие другие ситуации. Среди которых есть пункт: 6.8. Avoid using the Node.js crypto library for handling passwords, use Bcrypt
Пароли или секреты (ключи API) должны храниться с использованием безопасной функции hash + salt , такой которую предоставляет bcrypt , которая должна быть предпочтительным выбором по сравнению с реализацией JavaScript из-за соображений производительности и безопасности.
Также обратим внимание на призыв не использовать Math.random() в алгоритмах шифрования. Не будем углубляться, но кому интересно почитайте статью Майорова Случайные числа не случайны.
Но вернемся к crypto vs bcrypt. Нам предлагают использовать код:
// asynchronously generate a secure password using 10 hashing rounds bcrypt.hash('myPassword', 10, function(err, hash) // Store secure hash in user record >); // compare a provided password input with saved hash bcrypt.compare('somePassword', hash, function(err, match) if(match) // Passwords match > else // // Passwords don't match > >);
Давайте же последуем совету! И перепишем наше API на:
bcrypt.hash("whatthefoxsay", 10, function(err, hash) // Store secure hash in user record db.createUser(conn,'v7', hash) >); app.post('/api/v1/login', (req, res) => conn.all(` SELECT rowid AS id, login, password FROM users WHERE login = $login `, $login: req.body.login >, function (err, rows) if (err) res.send(err.message); > else if (rows && rows.length > 0) // compare a provided password input with saved hash bcrypt.compare( req.body.password, rows[0].password, function(err, match) res.send(match ? 'logged in' : 'bad news'); > ); > else res.send('bad news'); > > >) >)
Кажется что все выглядит пуленепробиваемое! Но!
Заметим один нюанс. Давайте попробуем устроить небольшой брутфорс пар логин+пароль по нашему API.
Воспользуемся подобной функцией:
const sendRequest = function (login, password) const result = start: Date.now(), login, password > const promise = new Promise(function (res, rej) request.post( 'https://127.0.0.1:3030/api/v1/login', json: login, password > >, function (error, response, body) if (!error && response.statusCode == 200) result.end = Date.now(); res(result); > > ); >) return promise; >
И отправим по 10 запросов на каждый из логинов v7 , v7_wrong , v7_wrong2
$ node ./attaker.js v7 917 v7_wrong 40 v7_wrong2 39
хмм.. давайте отправим еще раз:
$ node ./attaker.js v7 837 v7_wrong 42 v7_wrong2 45
Заметим что ответы с логином v7 заметно отличаются от остальных. Что здесь происходит, а происходит то, что если пользователь мы угадали, дальше идет тяжелый, надежный алгоритм bcrypt , который шифрует нам password из запроса и пытается сравнить с хэшем из БД. А давайте попробуем вернуть crypto и sha512 с солью:
$ node ./attaker.js v6 54 v6_wrong 32 v6_wrong2 30 finish
Заметим, что здесь разница не такая значительная, и если мы хостились на удаленной машине, а не на localhost , то погрешность в скорости прохождения пакетов по сети просто бы съела это 20-30 мс в 10 запросов. Значит ли это что bcrypt плох? Конечно нет. Мы просто неправильно его готовим, о чем, к сожалению best practice нам не говорит. На момент написания статьи, мы обсуждаем этот момент в issue, присоединяйтесь.
Но если мы изменим наш код таким образом, чтобы мы всегда вычисляли хэш. Например:
if (rows && rows.length > 0) // compare a provided password input with saved hash bcrypt.compare(req.body.password, rows[0].password, function(err, match) console.log('good'); res.send(match ? 'logged in' : 'bad news'); >); > else bcrypt.compare( req.body.password, "$2b$10$m.fhQdLyRI8ExS/GGh43FOkO.XTCS85QdVpn6sINdlxTGQSJe3Ydi", function(err, match) console.log('bad'); res.send('bad news'); > ); >
$ node ./attaker.js v7 786 v7_wrong 778 v7_wrong2 762 finish
Что вполне нас обезопасит от тайминговых атак. Кстати, по этой теме есть хорошая статья
usability vs security
Напоследок хочется рассмотреть еще один пример. Который не связан с аутентификацией напрямую. Но связан с формой регистрации. Сейчас фронтендеры ослеплены лучшими практиками UX, все стараются максимально ублажить пользователя и иногда в этой спешке теряются важные нюансы, о которых не стоит забывать.
Как пример, в статье предлагается уведомлять пользователя, после того как он ввел почту, зарегистрирован акк на этот email или нет. Это конечно красиво, но не до конца. Конечно мы можем добавить какие-то дополнительные проверки, на то чтобы выявить что к нам за проверкой почты стучится человек, а не машина. Но вы посмотрите за окно!
И есть такой волшебный инструмент как puppeteer, с помощью которого притвориться человеком гораздо проще. И мы можем брутфорсом пройти по форме регистрации и собрать базу email’ов пользователей, тех или иных ресурсов.
Для этого есть спасение — это ограничение количества запросов. Например как это делает github. На его ограничение даже можно наткнуться руками, если очень настырно пытаться его спрашивать.
Ссылки на источники
- Листинги кода и примеры описанные в статье
- Курс MIT «Безопасность компьютерных систем»
- Ответы юриста: как избежать ответственности за поиск уязвимостей
- Про Мэта Хонан
- psycopg2: The problem with the query parameters
- Тайминговая атака на Node.js — когда время работает против вас
- Исследование @ChALkeR
- Майоров «Случайные числа не случайны»
- Node.js best practices list
- Node.js Security Working Group
- Юзабилити форм авторизации
Уязвимости
Не ленитесь фильтровать входные данные, даже если ваш сайт «никому не нужен».
Ставить самые свежие версии ПО иногда опасно, но и на старых сидеть не стоит, потому что вас сможет взломать любой человек, умеющий пользоваться поиском и скачивать специальные программы «для взлома сайтов».
Фильтровать нужно не только HTML-теги и кавычки, но также и другую полученную от пользователя информацию. Особенно важно следить за логикой и никогда не верить пользователю! Например, можно с помощью «средств разработчика» вашем браузере просто сделать видимой скрытую форму и нажать кнопку. Возможно, вы не предусматривали подобный сценарий на своём сайте.
Защита данных сессии
По умолчанию вся информация сессий записывается в каталог temp. Если вы пользуетесь виртуальным хостингом, кто-то помимо вас может написать скрипт и считать данные сессий. Поэтому остерегайтесь хранения паролей или номеров кредиток в сессиях.
Если же всё-таки вам необходимо хранить подобные данные в сессии, то лучшей мерой будет шифрование. Это до конца не решает проблему, так как зашифрованные данные не на 100% безопасны, однако хранимая информация будет нечитабельной. Также вам стоит подумать о том, что данные сессии можно хранить в другом месте, таком, как база данных. В PHP есть специальный метод session_set_save_handler(), который позволит вам хранить данные сессий по-своему. Начиная с PHP 5.4 вы можете передать объект типа SessionHandlerInterface в session_set_save_handler().
Обработка ошибок
Во время разработки приложения стоит обращать внимание на все виды ошибок, которые могут возникнуть, однако, от конечных пользователей их нужно скрывать. Если же ошибки отображаются пользователям, то это делает ваш сайт уязвимым. Таким образом, лучшим решением будет различная конфигурация для конечного сервера и сервера разработки.
На публичном сервере вам необходимо отключить такие опции, как display_errors и display_start_up_errors, а вот такие опции как error_reporting и log_errors, должны быть активны, чтоб все ошибки, возникшие у пользователей, записывались в логи.
Защита подключаемых файлов
Часто в PHP скриптах происходит подгрузка других файлов, таких как подключение к базе и многих других. Некоторые разработчики дают таким файлам расширение .inc. Такие файлы по умолчанию PHP не парсит. Если обратиться к ним по адресу напрямую, пользователь сможет увидеть текст данного файла. Если же хакеру удастся получить доступ к файлу хранящиму данные подключение к базе, то в последствии он может получить доступ ко всем данным вашего приложения. Так что всегда используйте расширение .php для подгружаемых файлов и храните их там, куда нет прямого пользовательского доступа.
XSS или Межсайтовый скриптинг
Очень часто используемый тип уязвимостей. Для его использования взломщику нужно лишь иметь базовые знания HTML и JavaScript. В данном случае сервер не взламывается, а атака, по сути, направлена на пользователей уязвимого сайта.
Ключевая ошибка веб-разработчика в данном случае — недостаточная фильтрация полученных от пользователей данных.
XSS делится на две основные группы: «Активная XSS», это которая лежит где-то на сайте и ждёт свою жертву, а также «Пассивная XSS», которую взломщик посылает жертве, используя социальную инженерию.
Допустим, на сайте есть форма для ввода комментариев, которые сразу же отображаются после добавления. Злоумышленник может ввести комментарий, содержащий JavaScript код. После отправки формы, данные переправляются на сервер и заносятся в базу данных. После этого данные извлекаются из базы и новый комментарий отображается на HTML-странице, включая внедрённый JavaScript-код. Он может перенаправлять пользователя на какую-то вредоносную страницу или на фишинговый сайт.
Пример — На сайте пробуем ввести в текстовое поле фразу (см. ниже), жмём «Сохранить» и обновляем страницу. Находим мышку и пробуем ей подвигать возле почему-то пустого поля — появилось окошко? Поздравляю, мы нашли уязвимость!
" onmouseover="alert('XSS')" style https://ru.wikipedia.org/wiki/%D0%92%D0%BD%D0%B5%D0%B4%D1%80%D0%B5%D0%BD%D0%B8%D0%B5_SQL-%D0%BA%D0%BE%D0%B4%D0%B0">Википедия Такие уязвимости очень часто встречаются у начинающих веб-разработчиков. Для защиты нужно фильтровать кавычки и прочие спецсимволы, которые могут нарушить логику вашего запроса. Также, когда у вас есть число, обязательно явно приводите его к числу.
CSRF-уязвимости
Википедия
Атакующий использует различные трюки для получения конфиденциальной информации или совершения сделки без ведома жертвы. В основном это происходит на сайтах, где бизнес-логика строится на работе GET-запросов. GET-запросы должны использоваться только для получения доступа к информации, но ни в коем случае не для осуществления различного рода транзакций.
Следующий пример показывает как сайт может подвергнуться CSRF атаке:
Предположим, что Плохой Дядька хочет совершить CSRF-атаку над Алёной и сформировал специальный адрес, который отправил Алёне на e-mail:
Если Алёна авторизована на сайте example.com и пройдёт по данной ссылке, то с её счёта на счёт дядьки будет переведено $1000. В качестве альтернативы плохой дядя может отправить и изображение, а в атрибуте src внести "плохой" адрес.
Браузер не сможет отобразить данное изображение, так как его не существует, однако запрос будет совершён без ведома и участия Алёны.
В качестве дополнительных мер можно сгенерировать какой-то уникальный csrf-токен, который не должен знать злоумышленник, и прикреплять его к каждому POST-запросу. При входе пользователя в систему, можно генерировать случайный токен и записывать его в сессию. Поскольку все формы выводятся пользователю, данный токен нужно записывать в скрытое поле. Некоторые фреймворки вставляют этот токен в ваши формы автоматически.
XSS Game от Google
Google подготовила сайт для демонстрации уязвимостей - XSS Game. Нужно пройти несколько заданий. Если затрудняетесь, то даются несколько подсказок.
Level 1: Hello, world of XSS
Представлен сайт с поиском. Для начала введём поисковое слово, например, "cat". Выдаётся сообщение: "Sorry, no results were found for cat. Try again". При этом, также меняется адрес в адресной строке браузера https://xss-game.appspot.com/level1/frame?query=cat.
А что если попробовать ввести
cat
? Пробуем… Теперь выводится слово в виде заголовка, следовательно, нет проверки и обрезки специальных символов, и мы можем сделать простенькую инъекцию.
У нас есть два варианта. Например, можем сразу ввести адрес:
https://xss-game.appspot.com/level1/frame?query=
Подобный адрес можно послать пользователю, который выполнит вредоносный код. В нашем случае выскочит сообщение. Но вместо безобидной команды там может быть что-нибудь про document.cookie или другие плохие вещи.
Либо вводим в поиске команду:
Level 2: Persistence is key
На втором уровне представлен форум, где можно оставлять комментарии. Введённые данные сохраняются на сервере, например, в базах данных. Если не проверять вводимые данные, то можно получить уязвимость.
Давайте сразу попробуем с
cat
. Получилось! Пробуем . Не прокатило! Форум экранирует наш тэг . Но JS может быть вызван каким-нибудь элементом при разного рода событиях: onclick, onload, onerror и т.д. Можно попробовать встроить JS вместе с каким-либо элементом и вызвать его при событии с ним.
Попробуем, скажем, так. (В подсказках предлагают использовать img и атрибут onerror):
Проводим мышкой над несуществующей картинкой и получаем уязвимость. Переходим на следующий уровень.
Можно было ввести и другие команды, например:
Тогда бы у нас появилась кнопка, нажав которую, мы снова переходим на следующий уровень.
Level 3: That sinking feeling.
Третий уровень предлагает нам посмотреть на фотки облачного дата-центра).
Не всегда у нас есть возможность вводить данные в поисковых строках. В данном случае мы можем только щёлкать по картинкам. Но при этом мы замечаем, что меняется адрес в адресной строке с изменением номера картинки - https://xss-game.appspot.com/level3/frame#3. А что если попробовать написать другое число? Вроде ничего интересного. А если вместо числа написать слово? Вводим адрес.
https://xss-game.appspot.com/level3/frame#cat
На этот раз видим надпись об ошибке Image NaN и незагруженную картинку. Смотрим код страницы, чтобы увидеть адрес картинки:
Наше слово cat было добавлено в src картинки к слову cloud. А если пробовать закрыть тег, добавив спецсимволы? Попробуем так:
https://xss-game.appspot.com/level3/frame#cat' />
Выражением cat' /> будет закрыт атрибут src и при этом будет добавлен ".jpg' />". Значит, пора атаковать! В конец адреса добавим: :
https://xss-game.appspot.com/level3/frame#cat' />
Уязвимости сайтов
Уязвимости сайтов — это возможности их взлома из-за наличия ошибок в программном коде, неправильных настроек системы управления контентом (CMS) и операционной системы веб-сервера. Ежегодно компании тратят огромные деньги на поиск и устранение уязвимостей в своих веб-проектах, и неспроста: код сайтов почти всегда содержит изъяны. На данный момент классифицировано огромное количество ошибок безопасности в программном обеспечении для веб-ресурсов, не считая обнаруженных уязвимостей «нулевого дня».
Классификация уязвимостей сайтов
Можно выделить следующие основные виды уязвимостей сайтов:
- XSS (cross-site scripting, межсайтовое исполнение сценариев). Данная уязвимость позволяет запускать на сайте произвольный JavaScript-код с помощью форм ввода. Также XSS может выполняться с помощью редактирования GET-переменных в ссылках.
- SQL-инъекция. С помощью SQL-запросов хакер может сделать дамп базы данных или внести в нее изменения. Сами запросы обычно передаются с помощью форм ввода на сайте.
- Возможность внедрения или изменения HTML-кода сайта (опять же за счет полей и форм для ввода данных).
- Предсказуемое расположение каталогов и служебных файлов (например, панель управления администратора находится по адресу /admin), а также стандартные префиксы таблиц в базе данных.
- Выполнение команд ОС. Иногда бывает так, что с помощью сайта возможно исполнить команды на сервере, где он работает. Посредством этой уязвимости злоумышленник способен, например, запустить на сервере троянскую программу.
- Типовые имена учетных записей (admin, root) и / или слабые пароли доступа к панели администрирования. В этом случае может оказаться эффективным перебор паролей (брутфорс).
- Отсутствие таймаута сессии (Insufficient Session Expiration). Если лимит времени, по достижении которого неактивный пользователь считается вышедшим из системы, достаточно велик, то взломщик получает шанс использовать для входа старый идентификатор сеанса связи.
- Индексирование директорий (Directory Indexing). Если в каталоге на сервере нет страницы, выводимой по умолчанию (index.html / home.html / default.htm и т.п.), то при попытке обратиться к этой директории сервер выведет список подкаталогов, по которому можно свободно перемещаться с помощью обычного браузера.
- Небезопасное восстановление паролей (Weak Password Recovery Validation). Данная ситуация может возникнуть в том случае, если сервер дает возможность восстановить или изменить пароли других пользователей.
- Переполнение буфера (Buffer Overflow) позволяет с помощью перезаписи данных в памяти системы поменять путь исполнения программы. Данная уязвимость является одной из наиболее распространенных.
- Предсказуемое значение идентификатора сессии (Credential/Session Prediction). В этом случае у киберпреступника есть возможность перехватить сеанс другого посетителя сайта. Уникальный номер сессии предсказывается или угадывается.
- Подмена содержимого (Content Spoofing). Отдаваемые посетителю данные заменяются на поддельные, но пользователь по-прежнему думает, что отображаемые страницы созданы веб-сервером.
- Недостаточная аутентификация (Insufficient Authentication). За счет этой уязвимости злоумышленник может получить доступ к функциям сервера и важной информации, не имея соответствующих прав доступа.
- Отсутствие ограничений на количество попыток входа в систему. В такой ситуации робот злоумышленника может быстро и эффективно перебирать пароли к учетным записям.
Какие сайты наиболее уязвимы?
Согласно исследованию Positive Technologies, наиболее уязвимы сайты, написанные на PHP с использованием собственного ядра. В наибольшей степени защищены веб-ресурсы на Java/ASP.NET с использованием коммерческих CMS. В данном случае лучше заплатить больше, но быть уверенным в безопасности продукта. Цель киберпреступника — контроль над сайтом, а множественные уязвимости помогут в достижении его цели.
Как обнаружить и устранить уязвимости сайтов?
Источниками угрозы могут являться некачественный код сайта или сервера, CMS с открытым кодом, для которых злоумышленникам проще найти уязвимость и написать эксплойт. Важно следить за возможными уязвимостями на сайтах при помощи специальных средств. Для поиска ошибок безопасности в программном обеспечении для веб-ресурсов созданы автоматизированные расширения, дополнения и другие подобные средства, которые все делают вместо человека. Следует помнить, что инструментарий злоумышленников очень богат: у них есть программы и для автоматического тестирования каждого вида изъянов безопасности, и для поиска уязвимых сайтов, и для обнаружения брешей на сервере. Важно вовремя обновлять CMS сайта, ведь в новых версиях ядра часто исправляются различные уязвимости. Для того чтобы не пропустить обновления, можно использовать встроенные средства оповещения или подписаться на почтовую рассылку от разработчика CMS.