Криптография – это раздел математики, который применяется к информации для того, чтобы сделать ее значение непонятным. Она используется для сокрытия секретов и обеспечения конфиденциальности связи. Шифрование представляет собой двусторонний процесс, в котором можно сделать информацию нечитаемой, а затем «расшифровать» ее обратно в исходную форму. Хешированием называется односторонний процесс, то есть когда исходное значение восстановить невозможно.
Шифрование довольно часто используется для защиты секретов или для передачи данных, поскольку система потом требует эти данные обратно. Ценность представляют сами данные. К хешированию чаще всего прибегают для подтверждения личности, аутентификации в системе, проверки целостности данных, а также для решения некоторых задач или контроля. Фактически никому не интересно, какой у вас пароль, – важно его наличие: программе нужно знать, пускать вас в систему или нет. В данном случае ценностью являются не данные, а подтверждение личности (которое выполняется с помощью знания исходного значения, пароля). Хеширование значения также подразумевает, что в случае кражи использовать значение невозможно. Украденные пароли (в измененной и хешированной форме) не принесут вору никакой пользы, поскольку при вводе в систему они не будут распознаны как пароли.
ПРИМЕЧАНИЕ. В настоящее время существует опасение, что из-за квантовых вычислений наши нынешние формы криптографии и шифрования устареют, однако в данной книге предполагается, что этого еще не произошло. Неизвестно, когда это опасение станет реальностью, а на момент написания этой книги ни у кого, включая меня, нет доказанного рабочего алгоритма или стратегии шифрования, устойчивых к квантовым вычислениям. Таким образом, в книге мы не будем затрагивать эту тему.
Для обеспечения конфиденциальности данные должны быть зашифрованы при передаче (на пути к пользователю, базе данных, API и т. д.) и в состоянии покоя (в базе данных). Следует отметить, что таким образом сохранность секретов гарантирована. Если кто-то получит несанкционированный доступ к данным или перехватит трафик с помощью анализатора трафика (сниффера), он не сможет понять, что он нашел. Однако шифрование не защищает доступность или целостность данных. Кто-то все равно может удалить или изменить их в базе данных (изменение или удаление можно будет легко увидеть, но при неидеальной работе резервного копирования и восстановления это доставит ряд неудобств в ходе устранения последствий). Злоумышленник может перехватить трафик и изменить или заблокировать сообщения, что опять же приведет к проблемам. Тем не менее защита секретов (конфиденциальность в триаде CIA) представляет большую важность, поэтому, независимо от разрабатываемой системы, необходимо обеспечить шифрование (а не хеширование) данных при передаче и в состоянии покоя.
Кто-то может возразить, что данные следует шифровать даже во время их использования (в памяти), однако при работе с чрезвычайно чувствительными данными это, как правило, не является обязательным требованием к проекту. Для защиты особо важных данных рекомендуется чистить память перед выходом из программы, из системы или перед другим способом завершения работы.
Любые данные из входного потока системы с большой вероятностью могут быть взломаны или же привести к сбою или аварийному завершению работы приложения. Независимо от того, намеренно ли входным данным придали вредоносный характер или нет, из-за них приложение может перейти в неизвестное состояние (состояние, для которого не разрабатывался план действий), то есть попадает в большую опасность. С момента перехода приложения в неизвестное состояние злоумышленники могут управлять им как угодно, в том числе заставить его нарушить один или несколько аспектов CIA. Ваша программа должна уметь быстро и эффективно обрабатывать любой тип ввода, даже тот, который имеет вредоносный характер.
Под входными данными (вводом) подразумевается буквально все и вся, что не является частью приложения или что могло быть подвергнуто манипуляциям вне его.
ПРИМЕЧАНИЕ. Одним из основных рисков для компьютерного программного обеспечения является ситуация, когда данные (значения в переменных, из API или из базы данных) обрабатывают так, как будто они являются частью кода приложения. Запуск стороннего кода обычно называется «инъекционной» уязвимостью, которая признана многими специалистами угрозой номер один для безопасности программного обеспечения с самого начала существования нашей индустрии. Она является обоснованием для многих требований к проекту из этой главы.
Ниже приведены примеры входных данных приложения.
• Пользовательский ввод на экране (например, ввод поисковых фраз в поле).
• Информация из базы данных (даже специально разработанной для приложения).
• Информация из 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) или чего-то еще потенциально вредоносного. Всегда кодируйте выходные данные перед отображением на экране.
ПРИМЕЧАНИЕ. XSS – это внедренный в приложение JavaScript-код, выполняемый в браузере на устройстве, через которое пользователь просматривает веб-приложение. Если перед выводом на экран все выходные данные кодируются, XSS-атаки будут отображаться в виде текста и не выполняться, выдавая на экране нечто похожее на «<script>…». Это некрасиво, зато безвредно.
• Вы вызываете API: отправляете индекс, и обратно возвращается остальная часть адреса. Следует убедиться, что адрес имеет правильный формат и соответствует ожиданиям. Если он состоит из одних цифр или содержит символы, которые часто встречаются в коде ([, {, <, / и т. д.), то, скорее всего, он неверный. Адрес также не должен быть очень длинным: 500 символов достаточно для вызова любого API. Принимать входные данные следует только в том случае, если они прошли проверку. Наконец, данные, передаваемые между приложением и API, должны быть зашифрованы для защиты конфиденциальности пользователя. Шифрование может происходить в самом приложении посредством сервисной сетки или с помощью любого другого надежного механизма.
• Стороннее приложение вызывает ваше приложение и передает URL-адрес в параметрах. Это рискованное с точки зрения безопасности действие обычно называется открытым перенаправлением. По возможности приложение должно принимать информацию из внешних источников только по защищенным каналам (TLS), а также проверять получаемые данные. Лучше найти другой способ передачи данных, поскольку злоумышленник может изменить URL и отправить пользователей на опасный сайт. Если же это – единственный доступный вариант, следует перейти к главе 4 («Безопасный код»), где объясняется, как с ним работать.
• При написании приложения на небезопасном для памяти языке (например, C/C++) необходимо выполнять так называемую проверку границ, которая следит за тем, чтобы вводимые данные не переполняли ваши типы переменных. В C/C++ можно ввести больше максимальной суммы для целого числа, что приведет к его откату в отрицательные значения и, очевидно, вызовет проблемы. Также возможно переполнение строк, что приводит к известной уязвимости под названием переполнение буфера, когда злоумышленник может перезаписать часть памяти. В лучшем случае при переполнении буфера происходит сбой приложения, активирующий сигнал тревоги, и команда реагирования на инциденты блокирует действия злоумышленника. В худшем – злоумышленник использует информацию о сбое для корректировки и улучшения своей атаки, захватывает веб-сервер и проникает в сеть. Нельзя недооценивать риски при обсуждении возможностей переполнения буфера – данная категория уязвимостей требует серьезного отношения.
Важно, чтобы приложение сначала проверяло входные данные, а затем использовало их. Бессмысленно выполнять проверку данных после работы с ними. Проверка должна быть первым действием после поступления входных данных в приложение.
ПРИМЕЧАНИЕ. В случае вывода сообщения об ошибке на экран для отклонения пользовательского ввода, если вы решите показать пользовательский ввод, имейте в виду, что он может иметь вредоносный характер и, следовательно, вызвать сбой в работе программы. Всегда кодируйте вывод с помощью HTML-кодировки (эта функция доступна во всех современных системах программирования), если вы в контексте HTML.
СОВЕТ. Тип кодировки вывода зависит от контекста данных. Например, написанный в JavaScript-строках текст необходимо экранировать с применением Юникода. Однако, если вы встраиваете пользовательский ввод в обработчик событий, кодировка вывода будет состоять из двух уровней (JavaScript и затем HTML). По возможности избегайте подобных ситуаций либо изучите памятку от OWASP по профилактике XSS.
ПРИМЕЧАНИЕ. Если вы пишете или переписываете низкоуровневое приложение с нуля, всегда выбирайте язык Rust вместо C или C++. Rust – это новый язык программирования, который может выполнять низкоуровневые задачи так же хорошо, как C и C++, но, в отличие от них, Rust безопасен для памяти. Таким образом, при использовании этого языка проверка границ больше не требуется, а переполнение переменных для создания потенциальных уязвимостей безопасности становится невозможным. Безопасность памяти – это не шутка. По мнению создателя браузера Mozilla (Firefox), 73 % уязвимостей только в стилевом компоненте браузера никогда бы не возникли, если бы он был написан на Rust, а не на C/C++. Даже одно это проектное решение может очень сильно сократить поверхность атаки, что сводит на нет все приемлемые деловые аргументы, которые могли бы оправдать написание новых приложений на C, когда доступен Rust. «Но мы уже умеем программировать на C» является недопустимой причиной не изучать и не использовать Rust.