Защитное программирование как стиль написания программы

Обновлено: 07.07.2024

Рисунок 1 - Способы проявления ошибок

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

Защитное программирование – это такой стиль написания программ, при котором появляющиеся ошибки легко обнаруживаются и идентифицируются программистом.

Детальный анализ ошибок и их возможных ранних проявлений показывает, что целесообразно проверять:

правильность выполнения операций ввода-вывода;

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

Проверка правильности операций ввода-вывода.

Причины неверного определения исходных данных:

внутренние ошибки – ошибки устройств ввода-вывода или программного обеспечения;

внешние ошибки – ошибки пользователя.

ошибки передачи (аппаратные средства, например, вследствие неисправности, искажают данные);

ошибки преобразования (программа неверно преобразует исходные данные из входного формата во внутренний);

ошибки перезаписи (пользователь ошибается при вводе данных, например, вводит лишний или другой символ);

ошибки данных (пользователь вводит неверные данные).

Ошибки передачи обычно контролируются аппаратно.

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

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

Неверные данные может обнаружить только пользователь.

Проверка допустимости промежуточных результатов.

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

Предотвращение накопления погрешностей.

Чтобы снизить погрешность результатов вычислений, необходимо соблюдать следующие рекомендации:

избегать вычитания близких чисел (машинный ноль);

избегать деления больших чисел на малые;

сложение длинной последовательности чисел начинать с меньших по абсолютной величине;

стремиться по возможности уменьшать количество операций;

использовать методы с известными оценками погрешностей;

не использовать условие равенства вещественных чисел;

вычисления производить с двойной точность, а результат выдавать – с одинарной.

Обработка исключений.

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

2. Принципы защитного программирования

Существуют три основных принципа защитного программирования:

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

Немедленное обнаружение. Каждая ошибка должна быть выявлена как можно раньше, это упрощает установление ее причины.

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

3. Рекомендации по защитному программированию

Рекомендации по защитному программированию:

Делайте проверку области значений переменных.

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

Контролируйте итоги вычислений.

Включайте автоматические проверки (например, контроль переполнения или потери точности).

Один из моих читателей, Барри Гайлз, недавно написал мне и задал достаточно интересный вопрос, который, по моему мнению, достоин обсуждения:

«Недавно я столкнулся с одной интересной ситуацией на работе: я производил ревью кода и вставил защитные проверки — одну для проверки аргумента конструктора на null , одну для проверки на null значения, возвращаемого из свойства. У меня также имелись утверждения для закрытых методов, которые я использовал для того, чтобы явно указать мои предположения.
Похоже, что преобладающей практикой среди моих коллег по команде является опускание проверок и допущение падений. Если быть честным, я борюсь с этой концепцией, так как я уже привык разрабатывать посредством защитного программирования и считал это хорошей практикой. Я практически уверен, что дело обстоит так же в большей части руководств и записей в блогах».
«Вы не могли бы дать совет относительно того, почему лучше программировать в защитном стиле, вместо того, чтобы позволить коду провалиться и затем проверять трассировку стека?»

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

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

В этом простом классе ProductDetailsController , метод GetProductDetails создаёт экземпляр ProductDetailsViewModel , получая пользователя и информацию о продукте через инъекции репозиториев, конвертируя цену продукта в предпочитаемую пользователем валюту и возвращая данные о продукте, которые предполагается в дальнейшем отобразить на экране. Ради достижения цели статьи, давайте сконцентрируемся только на проблемах проверки на null . Как много вызовов могут пойти по провальному сценарию в методе GetProductDetails ? Как много объектов могут быть нулевыми ссылками?
Довольно много, как выясняется. Даже отделённый от своих зависимостей, этот небольшой кусочек кода может выбросить NullReferenceException , как минимум в шести случаях. Представьте, что вы получаете отчёт об ошибках с вашей системы, находящейся в эксплуатации. Трассировка стека указывает на метод GetProductDetails и типом выброшенного исключения является NullReferenceException . Какой из шести возможных объектов с нулевой ссылкой стал причиной ошибки?
Давая системе просто упасть, нам будет трудно ответить на этот вопрос. И не забывайте, что это всего лишь учебный пример. Большая часть эксплуатируемого кода, который я встречал, имел более 5 или 6 строк кода, так что, выяснение причины возникшей ошибки может быстро стать чем-то вроде поиска иголки в стоге сена.
Просто позволить коду упасть – это не очень полезно. Очевидно, если вы пишете очень короткие методы (практика, которую я строго рекомендую), возникающая проблема не так насущна, но чем ваши методы длиннее, тем менее профессионально, на мой взгляд, выражение «просто позволить коду упасть» звучит.

Возможно ли сбалансировать факторы, влияющие на проблему и её решение? Да, возможно, но для того, чтобы понять как, вы должны понимать корневую причину проблемы. Первоначальный пример кода не особенно сложен, но даже несмотря на это, присутствует множество случаев, когда этот код провалиться. Что касается нулевых ссылок, то причиной является ошибка в дизайне языка, но в целом, вопрос заключается в том, можете ли вы доверять входным данным или нет. Возвращаемые значения через вызов IUserRepository.Get это (косвенно) тоже входные данные.
В зависимости от того, в какой среде функционирует ваша программа, у вас либо есть возможность доверять входным данным, либо у вас такой возможности нет. Представим, на минуту, ситуацию, при которой ваше ПО эксплуатируется в «дикой местности». Вашим ПО может быть повторно используемая библиотека или фреймворк. В таком случае, вы вообще не можете доверять входным данным. Если это так, то, возможно, вы захотите применить принцип устойчивости и оставаться убеждённым в том, что вы действуете в защитном стиле не только относительно входных, но и выходных данных. Другими словами, вы не хотите передавать нулевые ссылки (или другие дьявольские значения) и другому взаимодействующему коду.
Пример кода, приведённый ранее, может передавать нулевые ссылки своим зависимостям, например, если userId = null, или (более изощрённо), если user.PreferredCurrency = null. Таким образом, следуя принципу устойчивости, вы должны были бы добавить ещё больше защитных выражений:

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

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

Так-то лучше, но ещё недостаточно хорошо… но, подождите: свойства для чтения это тоже возвращемые значения, так что мы и их можем не проверять:

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

Если возвращение null -ссылки является ошибкой, то каким тогда образом класс User , или класс Product , могут следовать принципу устойчивости? Тем же самым путём:

Заметьте, как класс User защищает свои инварианты. Свойство PreferredCurrency никогда не может быть null . Этот принцип также известен под другим названием: инкапсуляция.

Как всегда, помогает понимание факторов, лежащих в основе проблемы или понимание вашей кодовой базы. Вам следует писать значительно больше защитных конструкций в случае, если ваш код живёт в «дикой местности», чем тогда, когда он живёт в защищённой среде. По сей день, я считаю заблуждением веру в то, что вы можете обойтись написанием неряшливого кода; мы все должны быть программистами-рэйнджерами.
Бесструктурный защитный код вредит читаемости. Защитный код — это всего лишь ещё одна отговорка для написания спагетти-кода. Напротив, структурированное защитное программирование являет собой инкапсуляцию. Я знаю, что мне предпочесть.


Защитное программирование (defensive coding) — это стиль написания компьютерных программ, призванный сделать их более отказоустойчивыми в случае возникновения серьезных функциональных отклонений. Обычно подобное незапланированное поведение возникает из-за наличия багов в программе, но оно может быть обусловлено и совсем другими причинами: поврежденными данными, отказами аппаратного обеспечения, багами, которые возникают в программе в процессе ее доработки. Оказываясь в критической ситуации, код, написанный в защитном стиле, пытается принять максимально разумные меры с небольшим снижением производительности. Также такой код не должен допускать создания условий для возникновения новых ошибок.

Так есть ли разница между защитным программированием и обработкой ошибок?

История

Впервые я столкнулся с термином «защитное программирование» в книге Кернигана и Ритчи (The C Programming Language, 1st Edition). После тщательных поисков мне не удалось найти более ранних упоминаний этого термина. Вероятно, он был придуман по аналогии с «безопасным вождением», о котором стали активно рассуждать в начале 1970-х, за несколько лет до появления книги Кернигана и Ритчи.

В предметном указателе к книге K&R указано две страницы, на которых употребляется этот термин. На стр. 53 он означает написание кода, не допускающего возникновения багов, а на стр. 56 этот термин понимается уже немного иначе: создание кода, снижающего вероятность возникновения багов при последующих изменениях кода в процессе его доработки. В любом случае с тех пор термин «защитное программирование» употреблялся во многих книгах. Обычно под ним понимается обеспечение работоспособности кода даже при наличии багов — например, в книге «The Pragmatic Programmer» Эндрю Ханта и Дэйва Томаса (где о «программировании в защитном стиле» рассказывается в главе «Pragmatic Paranoia»), а также в других источниках.

Различия в толковании

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

Сравнение обработки ошибок и защитного программирования

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

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

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

Итак, выбираемая нами стратегия — защитное программирование или явное добавление обработки ошибок — зависит от области применения конкретной программы. Подробнее мы поговорим об этом в разделе «Область применения».

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

  1. программа принимает информацию прямо от пользователя, который может ввести невалидные данные;
  2. программа принимает данные из текстового файла, написанного человеком;
  3. программа принимает данные из XML-файла (сгенерированного автоматически или вручную);
  4. программа считывает файл с бинарными данными, созданный другой программой;
  5. программа считывает файл с бинарными данными, созданный ею же;
  6. программа считывает файл с бинарными данными, содержащий контрольную сумму для проверки наличия/отсутствия в нем повреждений;
  7. программа считывает временный бинарный файл, только что созданный ею же;
  8. программа считывает файл, созданный ею же и отображаемый в память;
  9. программа считывает информацию из локальной переменной (то есть из памяти), которую она только что записала.

В какой момент мы можем быть уверены, что данные не могут оказаться невалидными? Я считаю, что совершенно невозможен случай, в котором файл с невалидными данными продолжает генерировать верную контрольную сумму (см. сценарий 6). Тем не менее, если данные обладают повышенной критичностью с точки зрения безопасности, необходимо учесть и вероятность того, что файл был специально подправлен для получения «верной» контрольной суммы. В таком случае придется использовать криптографическую контрольную сумму, например SHA1.

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

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

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

Классический пример защитного программирования можно найти практически в любой программе, когда-либо написанной на C. Речь о случаях, когда условие завершения пишется не как тест на неравенство ( < ), а как тест на неэквивалентность (!=). Например, типичный цикл пишется так:

size_t len = strlen(str);
for (i = 0; i < len; ++i)
result += evaluate(str[i]);

size_t len = strlen(str);
for (i = 0; i != len; ++i)
result += evaluate(str[i]);

Очевидно, оба фрагмента должны работать аналогично, поскольку переменная 'i' может только увеличиваться и ни при каких условиях не может стать неравной 'len'. Почему же условия завершения цикла всегда пишутся только по первому образцу, а не по второму?

Во-первых, последствия возникновения «невозможного» условия очень пагубны и, вероятно, могут привести к всевозможным неприятным последствиям в готовой программе — например, к возникновению бесконечного цикла или нарушению доступа к памяти. Такое «невозможное» условие вполне может возникнуть в некоторых ситуациях:

  • плохое оборудование или залетный фотон гамма-излучения могут привести к тому, что один из битов 'i' случайным образом изменит состояние;
  • другой ошибочный процесс (в системе без аппаратной защиты памяти) или поток изменяет не принадлежащий ему фрагмент памяти;
  • ошибочный вышестоящий код (то есть код операционной системы или драйвера устройства) изменяет память;
  • функция 'evaluate' содержит вредоносный указатель, изменяющий значение 'i';
  • функция 'evaluate' повреждает указатель фреймового стека, и переменная 'i' оказывается в какой-то случайной точке стека;
  • при последующих изменениях кода возникают баги, например:

for (i = 0; i != len; ++i)
while (!isprint(str[i])) // патологическое изменение кода, при котором 'i' может никогда не оказаться равным 'len'
++i;
result += evaluate(str[i]);
>

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

Культура C

Есть еще два аспекта языка C, определяющих, как и когда в нем используется защитное программирование. Я имею в виду, во-первых, акцент C на эффективности кода и, во-вторых, применяемые здесь подходы к обработке ошибок.

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

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

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

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

Область применения

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

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

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

Симптомы

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

Проблемы защитного программирования

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

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

Гораздо хуже то, что защитное программирование скрывает ошибки и на этапах разработки и тестирования. Думаю, никто не считает, что это хорошо. Альтернатива — это использование подхода, иногда именуемого «агрессивное программирование» или «принцип быстрого отказа» (fail fast). Такие подходы нацелены как раз на быстрое проявление ошибок, а не на замалчивание.

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

Упражнение

Наконец, предлагаю вам пищу для размышлений. В стандартной библиотеке C есть функция, принимающая строку цифр и возвращающая целое число. Эта функция называется atoi.

Если вы не знакомы с atoi(), уточню, что она не возвращает никаких кодов ошибок, но останавливается, как только ей встречается первый же неожиданный символ. Например, atoi("two") просто возвращает нуль.

Является ли поведение atoi() примером защитного программирования? Почему?

Несколько месяцев назад Билл Гейтс объявил, что безопасность станет приоритетом номер 1 в Microsoft. Несколько групп в Microsoft, такие как Trusted Computing Group и Secure Windows Initiative стремятся улучшить безопасность в продуктах Microsoft, и, в конечном счете, улучшить ситуацию для людей и корпораций во всем мире. Если Microsoft, с ее миллиардными ресурсами и многочисленным штатом сотрудников, имеет проблемы с безопасностью, то как же решать проблему создания доверенных систем?

Создание защищенных систем.

Несколько месяцев назад Билл Гейтс объявил, что безопасность станет приоритетом номер 1 в Microsoft. Несколько групп в Microsoft, такие как Trusted Computing Group и Secure Windows Initiative стремятся улучшить безопасность в продуктах Microsoft, и, в конечном счете, улучшить ситуацию для людей и корпораций во всем мире. Эта инициатива не удивительна, принимая во внимание многочисленные уязвимости, найденные недавно в Windows XP, Internet Information Server, Internet Explorer и Outlook. Из-за популярности продуктов Microsoft и их значительной доли на рынке, уязвимости вызвали значительный ущерб повсеместно в Интернет. Если Microsoft, с ее миллиардными ресурсами и многочисленным штатом сотрудников, имеет все эти проблемы с безопасностью, то как же решать проблему создания доверенных систем?

Как скажет вам любой хороший профессионал по безопасности, невозможно создать полностью безошибочное и неуязвимое программное обеспечение. Ресурсы и финансы, требуемые для создания такого программного обеспечения, будут бесконечны. Основная идея в индустрии безопасности – уменьшение риска. То есть, уменьшение риска до приемлемого уровня. Эта статья содержит краткий обзор некоторых ключевых вопросов защищенного программирования. В ней показаны некоторые типичные ошибки, совершаемые при создании программ, которые ведут к их уязвимости. Далее представлен список рекомендаций, неукоснительное следование которым поможет избежать 90% проблем безопасности. И, в завершение – список ресурсов, которые помогут вам в задаче создания более защищенного программного обеспечения.

Защищенное программирование. Типичные ошибки

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

Переполнение буфера

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

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

Уязвимости форматной строки

Уязвимости форматной строки – новый класс проблем безопасности, обнаруженный в последние пару лет. Форматные строки – это программные конструкции, используемые в языках C и C++ для форматирования ввода-вывода. Они содержат специальные идентификаторы (такие как %s для строк, %d для целых чисел), которые, в случае использования в злонамеренном вводе, могут открыть информацию о содержании стэка и используемых в функции переменных. В частности, опасный идентификатор %n может использоваться для изменения данных в памяти. Поскольку это позволяет хакерам делать почти то же, что и при переполнении буфера, результаты одинаковы: запуск произвольного кода.

Первичная причина уязвимости форматной строки в использовании функций переменного параметра в C/C++. Эту проблему можно решить с помощью проверки на правильность ввода и поиска ошибок в коде. Также можно использовать автоматические утилиты проверки кода для обнаружения ошибок типа: printf (string); будет рекомендовано заменить на printf (“%s”, string).

Аутентификация

Аутентификация – наиболее важный компонент в любой защищенной системе. Некорректная аутентификация пользователя сделает все остальные функции безопасности, такие как шифрование, аудит и авторизация, просто бесполезными. Наиболее типичная ошибка в аутентификации – использование слабых мандатов (credentials), что позволяет хакеру применить метод грубой силы (brute force), например, к взлому пароля. Кроме того, необходима строгая политика паролей для уменьшения вероятности их взлома. Требования к составу пароля зависят от уровня безопасности, требуемого приложением и его функциями. Обычно рекомендуется, чтобы пароли были длиной минимум в 8 символов и включали буквы, цифры и спецсимволы.

Авторизация

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

Криптография

Если криптография производится в приложении, чувствительность данных очень высока. Опытных программистов, имеющих полное математическое понимание криптографических алгоритмов, как это ни странно и печально, очень мало. Одна ошибка в разработке криптографического алгоритма или его реализации может полностью подорвать защищенность приложения. Многие программы используют алгоритмы промышленного стандарта, такие как RSA или blowfish, но не некорректно работают с памятью, что оставляет возможным кражу пароля и других данных в чистом виде. Очень рекомендуется использовать CryptoAPI и CAPICOM, встроенные в операционные системы Windows, или купить библиотеки третьих лиц для избежания проблем с использованием криптографии. Очень легко по ошибке попасть в ловушку, считая, что данные «выглядят» зашифровано, и потому защищены.

Рекомендации для разработки защищенных программ

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

Не доверяйте пользовательскому вводу

Проверка правильности ввода

Необходимо разработать описание достоверных данных для каждого ввода. Пример для случая аутентификации: имя пользователя должно содержать буквы и цифры, быть длиной 6-20 символов, и пароль должен содержать 6-20 печатаемых символов. Если клиент посылает переполнение буфера более 20 символов, приложение должно отсоединить пользователя. Если производится атака SQL injection, проверка запроса в приложении не должна пропустить его к SQL серверу. Такое недоверие к пользовательскому воду в комбинации с хорошей проверкой корректности ввода в корне предотвратит большинство атак. Ваше приложение может иметь уязвимости SQL injection и переполнения буфера, но при правильной проверке корректности ввода оно может стать невзламываемым для подобных атак.

«Магические» ключи

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

Ресурсы для написания защищенных программ

Утилиты

Заключение

Разработка защищеннго программного обеспечения – непростая задача. Но, принимая во внимание количество атак сегодня, было бы преступной халатностью не создавать защищенный от хакеров код. Затраты, требуемые для создания защищенных программ, очень высоки. Тем не менее, обучая программистов избегать типичных ошибок, проводя анализ безопасности кода и проверку приложений на наличие ошибок безопасности, можно значительно улучшить код и избавиться от большинства типичных на сегодняшний день уязвимостей.

Читайте также: