вторник, 3 февраля 2009 г.

Defensive Design. Откуда берутся сбои

Так сложилось, что если о всяких паттернах и архитектурах на собеседованиях соискатели более или менее в состоянии поддержать разговор, то как только речь заходит о вариантах использования Debug.Assert(…), сразу же начинается плавание вокруг да около. Нет, я не говорю, что никто не может сформулировать основных положений «оборонительного проектирования», просто очень редко случаются действительно «концептуальные» беседы: как правило, приходится поднимать знания, которые присутствуют где-то на уровне подсознания на более высокий уровень. Поэтому я попробую организовать небольшую шпаргалку, дабы помочь коллегам.

Введение

Для начала вспомним знаменитый Закону Мёрфи (в общем виде) применительно к разработке ПО: «если существуют условия, в которых программа ведет себя неверно, то такие условия обязательно проявятся». Это может быть неверный пользовательский ввод, сбои в железе, рассогласование протоколов или же ошибки внутри самой программы. Так вот, оборонительное проектирование и борется со всевозможными проявлениями Закона Мёрфи – эта техника обороняет систему от того, что «никогда не может случиться».

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

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

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

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

Неверные данные

Этот принцип в частной форме очень хорошо знаком всем UI-программистам: пользователь всегда глуп. Никогда не стоит наедятся, что он введет емейл-адрес в правильном формате или будет аккуратно следить за максимально допустимым значением для какого-то поля. Поэтому программа должна защитить себя сама от подобных глупостей. Для этого UI-программисты используют всевозможные контролы-валидаторы. А более продвинутые коллеги реализуют целые подсистемы валидации данных.

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

В общем случае, есть три правила обработки входных данных:

  1. Проверке подлежат все данные из внешних источников;
  2. Проверке подлежат все входные параметры для данного модуля;
  3. Должна существовать четкая стратегия обработки неверных данных.

Плохой дизайн, грязный код

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

  1. Неоправданная сложность – потенциальный источник проблем (YAGNI, KISS, DRY);
  2. Повышение абстракции помогает изолировать проблемы (SOLID);
  3. Всестороннее тестирование, особенно на граничных и неверных условиях поможет выявить проблемы на ранних этапах (Software Testing);
  4. Чем меньше кода пишется с нуля – тем меньше в нем возникает ошибок (Code Reuse, Metaprogramming);
  5. Инспекция кода поможет выявить проблемы на уровне кодирования (Code Review, Code Audit).

Legacy-код, сторонние компоненты

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

Но даже хорошо написанный код унаследованных компонентов может стать источником проблем при использовании. Тривиальным примером является использование 16-битных инструкций в коде, который запускается на 64-битной операционной системе. Особенно, когда производители ОС заявляют о прекращении поддержки 16-битного API :) Еще одной знаменитой проблемой такого рода была «Проблема 2000».

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

В общем-то, все проблемы, связанные с legacy-кодом решаются установлением контроля над ним через изоляцию и рефакторинг. Изоляция позволяет нам максимально уменьшить зависимость и, как следствие, влияние проблемного компонента на систему в целом. А рефакторинг привносит в код недостающие элементы обороны.

Все эти техники подробно расписаны в хрестоматийной книге Working Effectively with Legacy Code (этот же материал, но в более компактной форме доступен в виде статьи).

Безопасность

Пользователь может быть не только глупым, как в случае с непредумышленным вводом неверных данных, но и вредным. Существует такая категория систем, сбой в которых может быть целью для некоторых пользователей. Это тот самый случай, когда смысл термина «оборонительное проектирование» оправдан на все 100%: разрабатываемая система должна быть устойчива к целенаправленным атакам.

Список рекомендаций для проектирования безопасных систем никогда не будет сформулирован окончательно. По сути, идет нескончаемая война между разработчиками и хакерами: война со своими стратегиями, героями, со своими победами и поражениями. И список рекомендаций было правильнее назвать списком тактик. Этот список будет пополняться по мере изобретения новых способов атак на системы. Приведу лишь самые основные из них:

  1. Любой код небезопасен, пока не доказано обратное;
  2. Безопасная обработка входных и выходных данных: доверяйте только проверенным источникам, ждите неприятностей на входе, никогда не сообщайте больше информации, чем нужно;
  3. Ограничивайте привилегии на уровне необходимого минимума для функционирования данного программного модуля;
  4. Неграмотная защита сродни ее отсутствию. Поэтому нужно защищаться от известных вам типов атак и всегда продолжать изучать новые типы атак;
  5. Пользуйтесь криптографией;
  6. Проводите регулярный аудит системы безопасности.

Паранойя

Как ни странно это прозвучит, избыточное увлечение оборонительными техниками чревато появлению брешей в обороне. Дело в том, что сам по себе код, который пишется для этих целей, может быть не лишен всех тех недостатков, которые присущи «основному» коду: код может быть достаточно сложным, медленным и полным всяких сюрпризов. Поэтому крайне важно научиться оценивать необходимый уровень защищенности системы.

Например, при разработке программы для работы с домашней медиа-библиотекой на 40 человеко-часов вряд ли имеет смысл проводить регулярные security audits по критериям, которые обычно предъявляются к банковским системам. Или же проводить скрупулезное тестирование с использованием эвристических алгоритмов, как если бы вы разрабатывали систему для систем жизнеобеспечения или авиадиспетчерских. Но в то же время, вполне оправдано внимание к защите приложения от некорректных входных данных.

Заключение

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

В следующих постах я продолжу «конспект», останавливаясь более детально на отдельных аспектах.