Принципы безопасного проектирования
Разберем ключевые принципы, которые помогают учитывать требования безопасности еще на этапе проектирования.
Когда вы разрабатываете ПО, то разбиваете задачу на мелкие части, которые взаимодействуют между собой. Например, на определенный набор классов и методов. Такой процесс называется архитектурным проектированием. Как и любой другой этап разработки, он не заканчивается после первого запуска приложения. Если архитектура оказывается неэффективной или возникают новые риски, приходится ее пересматривать.
Разные архитектуры дают разные преимущества: одни архитектуры проще поддерживать, другие — быстрее создавать, третьи — обеспечивают более высокий уровень безопасности. Принципы безопасного проектирования представляют собой четкие руководства, основанные на опыте и практике. Они служат своего рода правилами, которые помогают избегать неудачных решений и способствуют созданию качественной архитектуры. Однако важно понимать, что их соблюдение не гарантирует абсолютную безопасность. Такой подход лишь направляет мышление в нужное русло, но не заменяет его.
Бывают случаи, когда определенный принцип неприменим или противоречит другому. Например, простота — один из принципов безопасного проектирования. Но иногда приходится отказаться от нее в пользу более сложных систем, чтобы повысить уровень защищенности.
Тем не менее, если вы будете учитывать принципы безопасного проектирования при разработке ПО, это снизит количество угроз.
Разрабатывая архитектуру, необходимо определить, каким компонентам можно доверять, а каким — нет. Некоторые принципы проектирования касаются границы доверия. Это условная линия, которая разделяет компоненты, заслуживающие доверия, и компоненты, которым доверять не стоит. Расположение этой границы зависит от типа разрабатываемого ПО. Например, если вы создаете серверное приложение, то, скорее всего, будете доверять той инфраструктуре, в которой оно работает: серверу, операционной системе и среде выполнения контейнеров, если они есть. При этом вы не будете полностью полагаться на внешние клиентские системы, поскольку некоторые из них могут контролироваться злоумышленником. В таком случае граница доверия будет проходить между сервером и клиентами.
Многие основополагающие принципы безопасного проектирования были сформулированы еще в 1975 году Джеромом Х. Зальтцером и Майклом Д. Шредером в их работе «Защита информации в компьютерных системах». Примечательно, что этот список принципов выдержал проверку временем и остается актуальным до сих пор. С тех пор были предложены и другие принципы, но давайте начнем с основ.
Зальцер и Шредер сосредоточились на механизмах безопасности — то есть на той части системы, от которой зависит ее защищенность. Вот их список принципов безопасного проектирования.
Наименьшие привилегии (Least privilege). Каждый пользователь и приложение должны работать, используя как можно меньше привилегий (прав и уровней доступа). Этот принцип ограничивает ущерб от случайного события, ошибки или атаки. Он также уменьшает количество потенциальных взаимодействий между привилегированными приложениями, поэтому вероятность непреднамеренного, нежелательного или неправильного использования привилегий снижается.
Полное посредничество (Complete mediation), также известный как принцип невозможности обхода (Non-bypassability). Каждая попытка доступа должна быть проверена. Разверните механизм авторизации так, чтобы его нельзя было обойти.
Простота механизма (Simplicity of mechanism). Система, в особенности та ее часть, от которой зависит безопасность, должна быть максимально простой и легковесной.
Открытый дизайн (Open design). Механизм безопасности не должен зависеть от незнания злоумышленников. Вы должны действовать так, как будто он общеизвестен, и полагаться на конфиденциальность относительно небольшого количества легко изменяемых элементов, таких как пароли или закрытые ключи. Злоумышленник не должен иметь возможность взломать систему только потому, что он знает, как она работает. «Безопасность через неизвестность», как правило, не работает.
Безопасные конфигурации по умолчанию (Fail-safe defaults). Конфигурация по умолчанию должна быть такой, чтобы даже в случае сбоя или атаки система возвращалась в безопасное состояние. Если нет уверенности, что что-то должно быть разрешено, не разрешайте это.
Разделение привилегий (Separation of privilege). Доступ к объектам должен зависеть более чем от одного условия — например, наличия пароля. Таким образом, если злоумышленнику удастся нарушить одно условие, система останется в безопасности.
Примечание: иногда программы разбиваются на части, каждая из которых имеет свои привилегии. Такой подход иногда путают с разделением привилегий, но это не совсем верно. В данном контексте разделение программы на части с разными привилегиями является примером реализации принципа наименьших привилегий.
Минимизация совместного использования (Least common mechanism). Следует минимизировать количество общих ресурсов и их использование. Не стоит совместно использовать файлы, каталоги, ресурсы ядра операционной системы или вычислительные ресурсы, которым вы не доверяете, поскольку злоумышленники могут использовать их в своих целях.
Простота использования, или психологическая приемлемость (Psychological acceptability). Человеко-машинный интерфейс должен быть разработан таким образом, чтобы им было легко пользоваться. Это позволит пользователям регулярно и по умолчанию корректно применять механизмы безопасности.
Рассмотрим некоторые из этих принципов подробнее, поскольку их применение имеет последствия, которые могут быть не очевидны.
Принцип наименьших привилегий
Принцип наименьших привилегий – один из ключевых принципов безопасного проектирования. Его суть состоит в том, что каждый пользователь (будь то человек или приложение) должен работать, используя минимально необходимый набор привилегий. То есть не следует разрешать чтение или запись информации, если это не требуется для конкретного пользователя.
Этот принцип ограничивает потенциальный ущерб от возможной атаки и упрощает взаимодействие, связанное с безопасностью. Он применим и к внутренней структуре приложения: только та его часть, которой действительно необходимы привилегии, должна их иметь. Однако важно понимать, что реализация этого принципа не должна приводить к чрезмерному усложнению ПО. Простота в использовании — значимый критерий.
Вот несколько способов реализовать наименьшие привилегии, в зависимости от обстоятельств.
- Не предоставляйте приложению никаких особых привилегий (если это возможно). Это самый безопасный вариант. Например, в Linux есть возможность создания приложений с установленным битом setuid или setgid, что дает приложению привилегии владельца при запуске. Однако если есть возможность избежать использования этого механизма, стоит рассмотреть более безопасную альтернативу — требовать от пользователей входа в систему с особыми привилегиями через инструмент sudo. Это позволяет контролировать доступ к командам и действиям с повышенными привилегиями.
- Минимизируйте особые привилегии приложения и доступные ему данные. В Linux можно запустить приложение от имени специальной группы или пользователя с ограниченными правами вместо использования привилегированного пользователя (например, root). Это минимизирует потенциальные риски. Если ваше приложение обращается к базе данных, ограничьте права пользователя базы данных. Если ваша система баз данных использует SQL, используйте команду SQL GRANT для ограничения привилегий приложения. В Redis для этого можно использовать команду ACL Redis.
- Постоянно отказывайтесь от привилегий. Если вы используете сохраненные в Linux идентификаторы групп, идентификаторы пользователей или возможности, откажитесь от этих дополнительных привилегий как можно скорее. Это поможет предотвратить использование этих привилегий злоумышленником в случае атаки.
Если вы не можете окончательно отказаться от привилегий, постарайтесь свести к минимуму время, в течение которого привилегия активна. Это менее эффективно, поскольку некоторые атаки могут заставить приложение выполнять произвольный код. Однако количество выполняемых операций может быть ограничено, и минимизация времени действия привилегии уменьшит возможности злоумышленника.
- Разбейте приложение на модули и предоставьте особые привилегии только одному или нескольким модулям. В идеале привилегированный модуль не должен полностью доверять другим частям приложения (это называется взаимно подозрительным дизайном). Это поможет ограничить возможности злоумышленника в случае, если он взломает какую-либо часть приложения. Например, вы можете отделить часть приложения, реализующую графический интерфейс, от другой части с привилегиями. Механизмы разделения, такие как контейнеры, виртуальные машины, Linux seccomp и различные виды защитных оболочек, помогут вам разделить части приложения так, что взлом одной части не обязательно приведет к взлому другой. Однако будьте осторожны: убедитесь, что вы корректно настроили эти механизмы. Но не думайте, что их использование автоматически делает ваше ПО безопасным.
- Минимизируйте поверхность атаки — набор ресурсов и операций, к которым может получить доступ злоумышленник. Если вы разрешаете публичный доступ к некоторому методу, вы предоставляете доступ к нему всем злоумышленникам. По возможности ограничьте неавторизованный доступ к ресурсам и операциям. Если такой доступ не нужен, не предоставляйте его. В частности, не оставляйте отладочные операции в производственных системах, к которым может получить доступ злоумышленник.
- Валидируйте (проверяйте) вводимые данные, прежде чем принять их. Конечно, необходимо убедиться, что злоумышленники не смогут обойти эту проверку. Это настолько важная проблема, что у нее есть свой собственный принцип — полное посредничество, также известное как невозможность обхода.
Рассмотрим несколько примеров применения принципа наименьших привилегий.
Пример 1. При разработке веб-приложений важно ограничить доступ пользователей к критичным файлам, таким как конфигурационные и включаемые файлы сервера. Эти файлы могут содержать чувствительную информацию, например, пароли. Если вы используете традиционный веб-сервер, рекомендую хранить все критичные файлы вне DOCROOT, чтобы предотвратить прямой доступ к ним пользователей.
Пример 2. По умолчанию не разрешайте пользователям записывать системные конфигурационные файлы, и, где это возможно, реализуйте запрет на чтение файлов обычными пользователями. Проблема в том, что системные администраторы могут поместить пароли и ключи в конфигурационные файлы. Если есть причины предоставить более широкие права на чтение некоторой информации о конфигурации системы (например, в /etc), подумайте о создании каталога конфигурации системы вместо файла конфигурации системы, где имя каталога условно заканчивается на .d.
Каталоги конфигурации системы позволяют менеджерам пакетов добавлять и удалять определенные файлы конфигурации. Они не только снижают риск ошибок, но и позволяют ограничить права доступа к определенным файлам — например, с секретными ключами и паролями. Если вы используете системный каталог конфигурации, разрешить пользователю чтение будет менее проблематично, поскольку защитить секретные ключи и пароли гораздо проще.
Принцип полного посредничества
Каждый раз, когда приложение получает запрос, особенно если он исходит от источника, которому приложение не может полностью доверять, оно должно проверить запрос. Например, убедится в авторизации и в том, что входные данные действительны. Этот принцип также называют принципом отсутствия возможности обхода, так как смысл заключается в том, что злоумышленник не должен иметь возможность обойти проверки безопасности.
Распространенная ошибка — попытка запустить проверку безопасности в системе, которую может контролировать злоумышленник. Это позволит ему легко обойти любые проверки.
Простым примером небезопасной архитектуры является ситуация, когда веб-приложение на стороне сервера отправляет клиенту HTML, а в HTML включено требование проверки. HTML может содержать утверждение, требующее, чтобы максимальная длина сообщения не превышала 100:
<input id="name" type="text" maxlength="100">
Такой HTML подойдет, если его цель — быстрая проверка для борьбы со случайными ошибками. Но поскольку злоумышленники контролируют свой собственный веб-браузер, эту проверку легко обойти. Злоумышленник может отправить гораздо более длинный ввод. Вы не можете полагаться на то, что веб-браузер выполнит за вас любую проверку, связанную с безопасностью.
Вы также можете отправить JavaScript клиенту и выполнить некоторые проверки безопасности в браузере — например, для обеспечения быстрой обратной связи. Но и в этом случае вам придется выполнять все связанные с безопасностью входные проверки на сервере, даже если некоторые из них должны были выполняться на клиенте. На самом деле входные валидации не дублируют проверку на стороне клиента, потому что таким проверкам нельзя доверять.
Дело не в том, что «сервер хороший» или «клиент плохой». Важно другое: любой код, которому вы доверяете, должен выполняться только в доверенной среде.
Можно ли попытаться запустить свой код в среде, которой вы не доверяете? Теоретически, да. Некоторые разработчики действительно делают так, пытаясь защитить код при помощи обфускации. То есть используют инструменты, которые делают код сложным для понимания, надеясь, что это затруднит его анализ и модификацию. Однако следует помнить, что обфускацию можно обойти, и злоумышленники могут легко это сделать с помощью специальных инструментов.
Можно использовать гомоморфное шифрование, которое позволяет выполнять код, в то время как данные остаются зашифрованными. Но в настоящее время такое шифрование применимо только в особых случаях. Оно на порядки медленнее и гораздо сложнее.
Механизмы процессора Intel Software Guard eXtensions (SGX) должны обеспечивать безопасное выполнение кода и хранение данных, но на практике они оказались не такими безопасными.
Таким образом, создание системы с проверками безопасности, которые можно обойти, — опасная ошибка. Вот краткий перечень признаков, которые могут указывать на подобные ошибки.
- HTML, JavaScript или другие данные, отправленные клиенту, которые выполняют проверку ввода на стороне клиента. Это нормально, но только если все эти проверки выполняются повторно в доверенной среде.
- Есть риски, если клиентское приложение получает прямой сетевой доступ к базе данных. Необходимо убедиться, что все операции, которые может выполнять пользователь, авторизованы. Во многих системах контроль за операциями можно осуществлять с помощью команды SQL GRANT. Еще лучше организовать опосредованный доступ к базе данных через приложение, а не предоставлять прямой доступ, который может затруднить проверку вводимых данных и нарушить принцип наименьших привилегий. Если вы предоставляете прямой доступ к базе данных, рассмотрите возможность ограничения привилегий. Например, разрешите доступ только к представлению, предназначенному только для чтения, а не ко всей базе данных.
- Если злоумышленник может перехватить передаваемые по сети данные, это создает серьезную угрозу безопасности. Правильно настроенные сетевые соединения, защищенные современными версиями протоколов TLS (например, HTTPS) и SSH, устойчивы к перехвату.
Другие принципы безопасного проектирования
В дополнение к основным принципам безопасного проектирования существуют и другие важные принципы, которые следует учитывать при разработке ПО. Они помогают избежать распространенных ошибок.
Остерегайтесь состояний гонки
Состояние гонки (race condition) возникает, когда корректное поведение системы зависит от порядка выполнения определенных операций, но этот порядок не контролируется должным образом. Такие ситуации часто возникают, когда несколько процессов или потоков обращаются к одному и тому же общему ресурсу, но механизм контроля доступа к этому ресурсу не обеспечивает необходимой синхронизации.
Отсутствие должного контроля может привести к возникновению уязвимостей ПО. Чтобы обеспечить безопасность, многие приложения должны выполнять две ключевые задачи: проверять, авторизован ли запрос, и, если запрос авторизован, выполнять соответствующие действия.
Если злоумышленник может вмешаться в работу приложения между этими двумя этапами, оно может ошибочно разрешить действие, которое не было авторизовано. Обычно это происходит при работе с файловой системой или правами доступа. Эта проблема безопасности настолько распространена, что ей дали название — условие гонки «время проверки – время использования» (Time-of-check to time-of-use, TOCTOU).
Эффективный метод борьбы с условиями гонки TOCTOU — разработка и использование API, которые способны одновременно проверять авторизацию и выполнять требуемое действие. Это предотвращает возможность вмешательства злоумышленника между этапами проверки и выполнения.
Например, при создании файлов или других объектов с ограниченными привилегиями важно избегать ситуаций, когда файл сначала создается с высокими привилегиями, а затем уровень привилегий уменьшается. Вместо этого рекомендуется создавать объекты с минимально необходимыми привилегиями и расширять их по мере необходимости. Такой подход минимизирует окно возможностей для злоумышленника, исключая возможность использования избыточных прав в период между созданием объекта и повышением его привилегий.
При написании программ для Unix-подобных систем избегайте использования системного вызова access() для проверки возможности открытия файла перед последующим вызовом open(). Вместо этого организуйте код таким образом, чтобы непосредственно вызывать open(), поскольку этот системный вызов уже включает проверку разрешения доступа;
Чтобы гарантировать создание нового файла в Unix-подобной системе и предотвратить возможность его предварительного создания злоумышленником, запрашивайте эксклюзивное создание файла. Это достигается флагом O_EXCL в API open() языка C или буквы x в функции fopen() в других языках программирования. Эти механизмы гарантируют, что попытка создания файла будет успешной только в том случае, если файл еще не существует.
В Unix-подобных системах часто встречается проблема небезопасного создания временных файлов. Злоумышленник может повлиять на процесс создания и именования файлов, если временные файлы создаются в каталоге, доступном для модификации. Если злоумышленник создает файл до того, как приложение запросит создание временного файла без использования опции эксклюзивности, приложение может повторно использовать существующий файл, контролируемый злоумышленником. Использование опции exclusive само по себе не обеспечивает полную защиту от подобных атак, поскольку может привести к отказу в обслуживании.
Для обеспечения безопасности используйте простой цикл, который генерирует случайное имя файла в предполагаемом каталоге, а затем пытается создать эксклюзивный файл с максимально ограниченными привилегиями.
В большинстве языков программирования существуют инструменты и методы для безопасного создания временных файлов. В Python для этого используется модуль tempfile, который предоставляет функции для безопасного создания временных файлов и каталогов. В сценариях командной оболочки команда mktemp также может использоваться для безопасного создания временных файлов.
Применяйте механизмы повышения безопасности
Ошибки случаются. Чтобы сделать приложение более устойчивым к возможному появлению ошибок и уменьшить вероятность превращения этих ошибок в уязвимости, важно следовать принципу наименьших привилегий и применять различные механизмы повышения безопасности.
Один из таких механизмов — политика безопасности содержимого (Content Security Policy, CSP), которая ограничивает загрузку ресурсов с определенных URL-адресов и типов файлов, уменьшая тем самым поверхность атаки. Рандомизация разметки адресного пространства (Address Space Layout Randomization, ASLR) перемешивает расположение важных областей памяти в процессе работы приложения, затрудняя поиск уязвимостей. Важно понимать, что включение или предоставление возможности включения таких механизмов может существенно повысить уровень защиты системы.
Храните секреты в тайне
Чтобы обеспечить конфиденциальность секретов (приватных криптографических ключей, токенов и паролей), важно принять меры предосторожности. В частности:
- избегайте размещения секретов в исходном коде, поскольку исходный код управляется системами контроля версий и может быть доступен бОльшему количеству людей и систем, чем вы предполагаете;
- для обеспечения безопасности паролей, используемых для аутентификации пользователей, используйте специализированные алгоритмы хэширования, такие как Argon2, bcrypt или PBKDF2. Эти алгоритмы предназначены для безопасного хранения паролей;
- используйте защищенное TLS-соединение для взаимодействия клиента и сервера. За счет шифрования передаваемых данных оно предотвращает утечку чувствительной информации;
- при работе с секретами избегайте передачи их в качестве параметров командной строки, если это возможно. Параметры командной строки могут быть видимы другим процессам в системе, что представляет угрозу безопасности.
Разделяйте данные и код
Отделяйте пассивные данные от исполняемого кода, чтобы предотвращать возможность выполнения вредоносного кода, если злоумышленник сможет внедрить его в данные. Это еще один способ реализации принципа наименьших привилегий, который заключается в ограничении прав доступа данных до минимума, необходимого для их корректной обработки.
Одним из ярких примеров является политика безопасности содержимого (CSP), поддерживаемая современными веб-браузерами. CSP позволяет явно указать, что отправляемый HTML-код представляет собой исключительно данные, и запрещает использование встроенных скриптов или стилей, которые могут содержать вредоносный код. Вместо этого скрипты и стили могут быть загружены только из заранее определенных доверенных источников. Даже если злоумышленник сможет подменить HTML-код, он не запустит внедренный в него вредоносный код, поскольку CSP ограничивает возможности выполнения кода только из разрешенных источников.