-40%

Безопасность веб-приложений. Исчерпывающий гид для начинающих разработчиков

Text
Leseprobe
Als gelesen kennzeichnen
Wie Sie das Buch nach dem Kauf lesen
Schriftart:Kleiner AaGrößer Aa

Требования

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

• Содержит ли система конфиденциальные, чувствительные или персональные данные (PII, Personally Identifiable Information – идентифицируемая личная информация) или контактирует с ними?

• Где и как будут храниться данные? Будет ли приложение доступно для общественности (находиться в интернете) или только для внутреннего пользования (находиться в интранете)?

• Выполняет ли приложение конфиденциальные или важные задачи (например, перевод денег, отпирание дверей или доставку лекарств)?

• Выполняет ли приложение какие-либо программные действия, сопряженные с риском (например, позволяет ли пользователям загружать файлы)?

• Какой уровень доступности необходим?

• Требуется ли 99,999 % времени непрерывной работы? (Примечание: практически ни одна система не требует такого уровня доступности.)

В идеале представитель службы безопасности должен задать необходимые вопросы и на основании ответов к ним добавить соответствующие пункты в общий список требований к проекту. Например, «Позволит ли приложение загружать файлы пользователям? Да? Хорошо, тогда добавим вот такие требования безопасности в спецификацию проекта, чтобы разработка с самого начала велась с учетом обеспечения безопасности».

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

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

Шифрование

Криптография – это раздел математики, который применяется к информации для того, чтобы сделать ее значение непонятным. Она используется для сокрытия секретов и обеспечения конфиденциальности связи. Шифрование представляет собой двусторонний процесс, в котором можно сделать информацию нечитаемой, а затем «расшифровать» ее обратно в исходную форму. Хешированием называется односторонний процесс, то есть когда исходное значение восстановить невозможно.

Шифрование довольно часто используется для защиты секретов или для передачи данных, поскольку система потом требует эти данные обратно. Ценность представляют сами данные. К хешированию чаще всего прибегают для подтверждения личности, аутентификации в системе, проверки целостности данных, а также для решения некоторых задач или контроля. Фактически никому не интересно, какой у вас пароль, – важно его наличие: программе нужно знать, пускать вас в систему или нет. В данном случае ценностью являются не данные, а подтверждение личности (которое выполняется с помощью знания исходного значения, пароля). Хеширование значения также подразумевает, что в случае кражи использовать значение невозможно. Украденные пароли (в измененной и хешированной форме) не принесут вору никакой пользы, поскольку при вводе в систему они не будут распознаны как пароли.

ПРИМЕЧАНИЕ. В настоящее время существует опасение, что из-за квантовых вычислений наши нынешние формы криптографии и шифрования устареют, однако в данной книге предполагается, что этого еще не произошло. Неизвестно, когда это опасение станет реальностью, а на момент написания этой книги ни у кого, включая меня, нет доказанного рабочего алгоритма или стратегии шифрования, устойчивых к квантовым вычислениям. Таким образом, в книге мы не будем затрагивать эту тему.

Для обеспечения конфиденциальности данные должны быть зашифрованы при передаче (на пути к пользователю, базе данных, API и т. д.) и в состоянии покоя (в базе данных). Следует отметить, что таким образом сохранность секретов гарантирована. Если кто-то получит несанкционированный доступ к данным или перехватит трафик с помощью анализатора трафика (сниффера), он не сможет понять, что он нашел. Однако шифрование не защищает доступность или целостность данных. Кто-то все равно может удалить или изменить их в базе данных (изменение или удаление можно будет легко увидеть, но при неидеальной работе резервного копирования и восстановления это доставит ряд неудобств в ходе устранения последствий). Злоумышленник может перехватить трафик и изменить или заблокировать сообщения, что опять же приведет к проблемам. Тем не менее защита секретов (конфиденциальность в триаде CIA) представляет большую важность, поэтому, независимо от разрабатываемой системы, необходимо обеспечить шифрование (а не хеширование) данных при передаче и в состоянии покоя.

Кто-то может возразить, что данные следует шифровать даже во время их использования (в памяти), однако при работе с чрезвычайно чувствительными данными это, как правило, не является обязательным требованием к проекту. Для защиты особо важных данных рекомендуется чистить память перед выходом из программы, из системы или перед другим способом завершения работы.

Никогда нельзя доверять входному потоку системы

Любые данные из входного потока системы с большой вероятностью могут быть взломаны или же привести к сбою или аварийному завершению работы приложения. Независимо от того, намеренно ли входным данным придали вредоносный характер или нет, из-за них приложение может перейти в неизвестное состояние (состояние, для которого не разрабатывался план действий), то есть попадает в большую опасность. С момента перехода приложения в неизвестное состояние злоумышленники могут управлять им как угодно, в том числе заставить его нарушить один или несколько аспектов CIA. Ваша программа должна уметь быстро и эффективно обрабатывать любой тип ввода, даже тот, который имеет вредоносный характер.

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

ПРИМЕЧАНИЕ. Одним из основных рисков для компьютерного программного обеспечения является ситуация, когда данные (значения в переменных, из API или из базы данных) обрабатывают так, как будто они являются частью кода приложения. Запуск стороннего кода обычно называется «инъекционной» уязвимостью, которая признана многими специалистами угрозой номер один для безопасности программного обеспечения[5] с самого начала существования нашей индустрии. Она является обоснованием для многих требований к проекту из этой главы.

Ниже приведены примеры входных данных приложения.

• Пользовательский ввод на экране (например, ввод поисковых фраз в поле).

• Информация из базы данных (даже специально разработанной для приложения).

• Информация из API (даже написанного самостоятельно).

• Информация, поступающая от другого приложения, с которым ваше приложение интегрируется или от которого принимает входной поток данных (сюда входят бессерверные приложения и скрипты).

• Значения в параметрах URL, значения cookie, файлы конфигурации.

• Данные или команды из облачных рабочих процессов.

• Изображения, взятые с других сайтов (с разрешением или без него).

• Значения из онлайн-хранилища.

Это не окончательный список примеров. Пожалуйста, имейте в виду: нанести вред может все, что не является частью вашей программы.

ПРИМЕЧАНИЕ. Облачные рабочие процессы обычно используются для вызова бессерверных приложений, но с их помощью можно запустить действие внутри приложения.

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

Элементы приложения, которыми можно манипулировать вне программы:

• параметры URL (их может изменить пользователь);

• информация в cookie, для которой не установлены флаги «безопасные» и «HTTPS only»;

• скрытые поля (они не защищены от злоумышленников);

• заголовки запросов HTTPS;

• введенные на экране значения, которыми можно манипулировать после пройденной проверки JavaScript с использованием веб-прокси (подробнее об этом позже);

• фронтенд-фреймворки, которые не являются частью приложения, а размещаются в интернете и вызываются в режиме реального времени;

• код сторонних разработчиков, который включается в состав приложения при его компиляции (библиотеки, вставки, платформы и т. д.);

• изображения, которые включаются в состав приложения и развертываются в другом месте в интернете;

• неуправляемые файлы конфигурации;

• API или любой другой сервис, к которому обращается приложение;

• неконтролируемые скрипты.

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

 

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

Вот примеры проверки данных из входного потока.

• Вы ожидаете ввод даты рождения, поэтому проверяете, что полученное значение действительно имеет формат даты или преобразовывается в формат даты, а также не выходит за рамки предыдущих 100 лет (например, age > current year – 100 && age < current year). Если значение не соответствует формату даты (например, «aaaaaaaa»), показывает, что человеку 5000 лет или что он еще не родился, то оно не принимается. При этом должны появиться соответствующие сообщения об ошибке: в случае неправильного формата необходимо сообщить о некорректном виде данных и указать ожидаемый формат, в случае выхода значения за пределы заданного диапазона в 100 лет – о неверном возрасте.

• Поле с лимитом в 80 символов предназначено для ввода имени человека. Необходимо убедиться, что количество символов ввода равно или не превышает 80, а символы соответствуют формату имени. Например, если значение содержит символы %, [, {, < или |, то вряд ли оно является настоящим именем, поэтому его нужно отклонить с соответствующим сообщением об ошибке. Однако, поскольку многие иностранные имена и фамилии содержат апострофы (‘), например O’Коннор, необходимо допускать такой ввод, но обрабатывать его осторожно (то есть закодировать его, о чем подробнее будет рассказано ниже). Также необходимо допускать диакритические знаки (é, å и т. д.), буквы из других алфавитов помимо латинского, дефисы и т. д.

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

• Ваша программа выполняет поиск в базе данных и выводит на экран строку из найденной записи. Эти входные данные следует проверить на соответствие ожиданиям точно так же, как если бы они были получены от пользователя. Они не должны содержать межсайтового скриптинга (cross-site scripting, XSS) или чего-то еще потенциально вредоносного. Всегда кодируйте выходные данные[6] перед отображением на экране.

ПРИМЕЧАНИЕ. XSS – это внедренный в приложение JavaScript-код, выполняемый в браузере на устройстве, через которое пользователь просматривает веб-приложение. Если перед выводом на экран все выходные данные кодируются, XSS-атаки будут отображаться в виде текста и не выполняться, выдавая на экране нечто похожее на «<script>…». Это некрасиво, зато безвредно.

• Вы вызываете API: отправляете индекс, и обратно возвращается остальная часть адреса. Следует убедиться, что адрес имеет правильный формат и соответствует ожиданиям. Если он состоит из одних цифр или содержит символы, которые часто встречаются в коде ([, {, <, / и т. д.), то, скорее всего, он неверный. Адрес также не должен быть очень длинным: 500 символов достаточно для вызова любого API. Принимать входные данные следует только в том случае, если они прошли проверку. Наконец, данные, передаваемые между приложением и API, должны быть зашифрованы для защиты конфиденциальности пользователя. Шифрование может происходить в самом приложении посредством сервисной сетки или с помощью любого другого надежного механизма.

• Стороннее приложение вызывает ваше приложение и передает URL-адрес в параметрах. Это рискованное с точки зрения безопасности действие обычно называется открытым перенаправлением. По возможности приложение должно принимать информацию из внешних источников только по защищенным каналам (TLS), а также проверять получаемые данные. Лучше найти другой способ передачи данных, поскольку злоумышленник может изменить URL и отправить пользователей на опасный сайт. Если же это – единственный доступный вариант, следует перейти к главе 4 («Безопасный код»), где объясняется, как с ним работать.

• При написании приложения на небезопасном для памяти языке (например, C/C++) необходимо выполнять так называемую проверку границ[7], которая следит за тем, чтобы вводимые данные не переполняли ваши типы переменных. В C/C++ можно ввести больше максимальной суммы для целого числа, что приведет к его откату[8] в отрицательные значения и, очевидно, вызовет проблемы. Также возможно переполнение строк, что приводит к известной уязвимости под названием переполнение буфера[9], когда злоумышленник может перезаписать часть памяти. В лучшем случае при переполнении буфера происходит сбой приложения, активирующий сигнал тревоги, и команда реагирования на инциденты блокирует действия злоумышленника. В худшем – злоумышленник использует информацию о сбое для корректировки и улучшения своей атаки, захватывает веб-сервер и проникает в сеть. Нельзя недооценивать риски при обсуждении возможностей переполнения буфера – данная категория уязвимостей требует серьезного отношения.

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

ПРИМЕЧАНИЕ. В случае вывода сообщения об ошибке на экран для отклонения пользовательского ввода, если вы решите показать пользовательский ввод, имейте в виду, что он может иметь вредоносный характер и, следовательно, вызвать сбой в работе программы. Всегда кодируйте вывод с помощью HTML-кодировки (эта функция доступна во всех современных системах программирования), если вы в контексте HTML.

СОВЕТ. Тип кодировки вывода зависит от контекста данных. Например, написанный в JavaScript-строках текст необходимо экранировать с применением Юникода. Однако, если вы встраиваете пользовательский ввод в обработчик событий, кодировка вывода будет состоять из двух уровней (JavaScript и затем HTML). По возможности избегайте подобных ситуаций либо изучите памятку от OWASP по профилактике XSS[10].

ПРИМЕЧАНИЕ. Если вы пишете или переписываете низкоуровневое приложение с нуля, всегда выбирайте язык Rust вместо C или C++. Rust – это новый язык программирования, который может выполнять низкоуровневые задачи так же хорошо, как C и C++, но, в отличие от них, Rust безопасен для памяти. Таким образом, при использовании этого языка проверка границ больше не требуется, а переполнение переменных для создания потенциальных уязвимостей безопасности становится невозможным. Безопасность памяти – это не шутка. По мнению создателя браузера Mozilla (Firefox), 73 % уязвимостей только в стилевом компоненте браузера никогда бы не возникли, если бы он был написан на Rust, а не на C/C++[11]. Даже одно это проектное решение может очень сильно сократить поверхность атаки, что сводит на нет все приемлемые деловые аргументы, которые могли бы оправдать написание новых приложений на C, когда доступен Rust. «Но мы уже умеем программировать на C» является недопустимой причиной не изучать и не использовать Rust.

Кодирование и экранирование

Наиболее известная уязвимость безопасности в веб-приложениях – это межсайтовый скриптинг (XSS). По оценкам, на данный момент он присутствует в более чем двух третях[12] веб-приложений в интернете. Существует несколько способов снижения данного риска: через заголовок политики безопасности контента (CSP), проверку ввода и кодирование вывода. Требование кодировки вывода существует только для предотвращения XSS-атаки, а из-за большой распространенности данной уязвимости, несомненно, стоит добавить в приложение все три перечисленных средства защиты.

СОВЕТ. Добавление в приложение всех трех способов борьбы с XSS – отличный пример применения принципа «защиты в глубину».

Экранирование символа или значения означает удаление специальных полномочий, которые он имел бы, если бы выполнялся как код, а не рассматривался как часть данных (как и должно быть). Обычно экранирование осуществляется путем добавления обратной косой черты (\) перед соответствующим специальным символом.

Кодированием называется преобразование исходного формата значения согласно используемому стандарту (кодирование URL, кодирование base64, кодирование HTML). Кодирование можно легко обратить, поэтому его не следует путать с шифрованием или использовать вместо него. Целью кодирования является не защита кодируемого значения, а изменение его оригинального формата на нужный для использования. Например, если вы собираетесь вывести какую-нибудь информацию на экран, вам необходимо закодировать ее, используя соответствующую функцию (все современные платформы имеют такую функцию). Если значение, выведенное на экран, содержит вредоносный код (например, атаку межсайтового скриптинга), то при произведенной кодировке выходных данных будет выведено только текстовое значение, а не интерпретация (выполнение) кода в браузере в качестве JavaScript. Кодирование выходных данных обезвреживает код XSS-атаки.

Заповедь здесь, скорее всего, очевидна: кодируйте (и, если нужно, экранируйте) все выходные данные.

 

ПРИМЕЧАНИЕ. XSS – это особый вид инъекционных уязвимостей, поскольку при успешной атаке код (JavaScript) выполняется на стороне клиента (в браузере), тогда как большинство других уязвимостей данного типа выполняются на стороне сервера (на уровне интерпретатора, операционной системы и т. д.). Защита от XSS подразумевает сочетание проверки входных значений и кодирования либо экранирования выходных значений, в то время как все остальные инъекционные уязвимости зависят в основном от проверки ввода и настроек конфигурации на стороне сервера. Кроме того, XSS встречается чаще других серьезных типов уязвимостей в интернете. Таким образом, несмотря на то что XSS является одним из видов инъекционных уязвимостей, его всегда выделяют в отдельный класс уязвимостей.

Сторонние компоненты

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

ИЗВЕСТНАЯ УЯЗВИМОСТЬ

Что значит «известная уязвимость»? По своему определению пользовательское программное обеспечение уникально. Каждая его часть – снежинка, и, следовательно, каждая новая уязвимость, которую можно в ней найти, неизвестна общественности. Программное обеспечение, которое не является пользовательским, часто называют просто программным обеспечением, как, например, операционная система или COTS. (COTS расшифровывается как Commercial/Customizable/Configurable Off The Shelf и буквально означает «Коммерческий/пользовательский/конфигурируемый готовый программный компонент».) Такое программное обеспечение может приобрести и установить любой желающий, и при этом оно обладает широкими возможностями конфигурирования. В качестве примера можно привести такие программы, как SharePoint, WordPress, Microsoft Office и Adobe Illustrator. В идеальном мире, когда тестировщик на проникновения, исследователь в области безопасности или другой «хакер» находит уязвимость в каком-либо программном обеспечении (пользовательском или нет), он сообщает о ней разработчику (это называется координированным раскрытием) или отмечает ее в программе Bug Bounty и получает вознаграждение. Если это не пользовательское программное обеспечение, то после сообщения об ошибке и выпуска исправления (обычно вместе с обновлением программного обеспечения) ошибка публикуется в интернете (например, в базе данных CVE Mitre[13]) для всеобщего обозрения и таким образом становится «известной» уязвимостью. Версия программного обеспечения с неисправленной уязвимостью, обычно обнаруживающейся сканирующими автоматическими инструментами, называется «имеющая известную уязвимость».

После обнаружения, но до выпуска исправления ошибка в программном обеспечении, операционной системе или COTS-компоненте называется «нулевым днем», или «0-day»: тем самым отмечается, что ошибка еще не исправлена. Часто команды безопасности говорят об исправлениях, используя понятие количества дней с момента обнаружения ошибки, например «этой уязвимости 90+ дней, а мы до сих пор не исправили ее!», поэтому был выбран вариант «ноль».

ПРИМЕЧАНИЕ. Если в подкасте или в новостной статье говорится, что кто-то «сбросил день», это значит, что была обнародована информация об уязвимости в системе безопасности, для которой не существует известного исправления. Обычно это делается для оказания социального давления, чтобы заставить компанию выпустить исправление, или для демонстрации своих навыков исследователя безопасности. По моему мнению, всем, кто располагает информацией об уязвимости в системе безопасности, следует всегда сообщать о ней разработчику программного обеспечения, прежде чем публиковать ее на общественных ресурсах. Не для защиты компании, которая производит ПО, а для защиты пользователей. Они ни в чем не виноваты, и подвергать их риску противоречит цели и обязательствам индустрии информационной безопасности.

Существует множество средств, позволяющих проверить наличие уязвимостей в сторонних компонентах. В этой книге мы не будем рекомендовать конкретные инструменты или поставщиков, но предложим стратегические варианты применения таких средств. Первая стратегия заключается в использовании двух инструментов, если вы можете себе это позволить. Они проверяют разные базы данных разными способами и поэтому могут уловить разные проблемы. Вторая стратегия заключается в регулярном (ежедневном или, по крайней мере, еженедельном) сканировании репозитория кода, а также сканировании при каждом запуске кода в производство. Сканировать репозиторий необходимо потому, что даже в редко обновляемых приложениях все равно постоянно обнаруживаются новые уязвимости. Чем старше компоненты, внедренные в код приложения, тем больше времени было у исследователей безопасности и злоумышленников для изучения и поиска в них уязвимостей. Следует проводить сканирование при каждой публикации кода, поскольку вы (или ваши коллеги), не зная, могли добавить в него новый компонент или обновить уже имеющийся компонент до версии, в которой есть известная уязвимость. Включение проверки на наличие уязвимостей в CI/CD-конвейер (или любой другой процесс, используемый для публикации кода) защитит вас от непреднамеренного внедрения вредоносного стороннего кода.

Обратите внимание, что эти действия подразумевают под собой проверку кода сторонних разработчиков на известные уязвимости. Скорее всего, еще больше уязвимостей остаются неизвестными. Проверка на известные уязвимости является необходимым минимумом в создании безопасного программного обеспечения. Если разрабатывающееся программное обеспечение требует очень высокого уровня надежности, нужно тестировать и просматривать каждую строчку кода, от которого оно зависит. Лучшая практика – включать в приложение только необходимые сторонние компоненты, а не каждую новую крутую штуку, увиденную в интернете. Несмотря на сложности при продаже такого приложения (давайте признаем, что новые технологии – это интересно и весело), если вместо простого «нет» вы сможете объяснить существующие риски и предложить альтернативу или решение, то скорее всего добьетесь положительных результатов.


ПРЕДУПРЕЖДЕНИЕ О НАРУШЕНИИ КОНФИДЕНЦИАЛЬНОСТИ

Алиса хотела прикрепить свой зашифрованный личный онлайн-календарь к программе настольного календаря, чтобы видеть рабочий и личный календари в одном месте. Она прочитала страницу справки для программы настольного календаря, в которой говорилось, что нужно перевести параметр «Общий доступ к календарю» в режим «Публичный». Алиса была потрясена! Ее календарь зашифрован, потому что она хочет сохранить приватность личной информации. Она оставила жалобу, в которой объяснила, что данная страница должна предупреждать пользователей о возможных проблемах с конфиденциальностью при такой настройке. В результате Алиса получила обновленную программу настольного календаря.

5«Инъекционная» уязвимость признана многими специалистами по безопасности угрозой № 1 для безопасности программного обеспечения: www.owasp.org/index.php/Top_10-2017_A1-Injection. ; www.owasp.org/index.php/Top_10_2013-A1-Injection. ; www.owasp.org/index.php/Top_10_2010-A1-Injection. .
6Кодирование выходных данных и предотвращение XSS-атак: portswigger.net/web-security/cross-site-scripting/preventing. .
7Проверка границ: en.wikipedia.org/wiki/Bounds_checking. .
8Целочисленное переполнение: en.wikipedia.org/wiki/Integer_overflow. .
9Переполнение буфера: en.wikipedia.org/wiki/Buffer_overflow. .
10Шпаргалка OWASP по профилактике XSS-уязвимости: owasp.org/www-project-cheat-sheets/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html. .
11Rust – безопасный для памяти язык программирования: acks.mozilla.org/2019/02/rewriting-a-browser-component-in-rust. .
12OWASP Топ-10 за 2017 год: 77 % приложений содержит XSS-уязвимости. www.ptsecurity.com/ww-en/analytics/web-application-vulnerabilities-statistics-2019. .
13База данных CVE Mitre: cve.mitre.org. .