Искусство оборонительного программирования

Автор оригинала: Diego Mariani
  • Перевод
image

Почему разработчики не могут написать безопасный код? Мы не говорим здесь очередной раз про «чистый код». Мы говорим о большем с чисто практической точки зрения — о надёжности и безопасности программного обеспечения. Да, потому что небезопасное программное обеспечение в значительной степени бесполезно. Посмотрим, что значит «небезопасное» программное обеспечение:

  • Полёт №501 ракеты «Ариан-5» Европейского космического агентства был прекращён через 40 секунд после старта (4 июня 1996 г.). Экспериментальная ракета-прототип стоимостью 1 млрд. долларов США самоликвидировалась из-за ошибки в бортовом ПО управления.
  • Ошибка в программе, управлявшей установкой лучевой терапии Therac-25, стала прямой причиной смерти, как минимум, пяти пациентов в 80-х годах, когда она задавала чрезмерные дозы рентгеновского облучения.
  • Программная ошибка в зенитном ракетном комплексе MIM-104 «Patriot», вызывавшая уход его системных часов на одну треть секунды за сто часов, привела к его неспособности обнаружить и перехватить летящую ракету. Иракская ракета попала в воинскую часть в г. Дахран, Саудовская Аравия (25 февраля 1991 г.), погибло 28 американцев.

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

Первое знакомство с защитным программированием (проектирование самотестирующих и самокорректирующих программ)


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

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

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


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

image

Лучшая защита — это нападение

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

Лучшая защита — это нападение. Будьте строгими.

Используйте абстрактное представление баз данных


Первой из топ-10 уязвимостей ПО согласно OWASP являются инъекции. Это означает, что кто-то (или много людей) ещё не использует безопасные инструменты для обращения к своим базам данных. Используйте пакеты и библиотеки абстрактного представления баз данных. В языке PHP можно использовать PDO (переносимый распределённый объект), чтобы обеспечить базовую защиту от инъекций.

Не изобретайте велосипед


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

Именно это называют разумным многократным использованием кода. Пользуйтесь этой возможностью.

Не доверяйте разработчикам


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

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

Пишите НАДЁЖНЫЙ код


Это трудное дело для (защитного) программиста — написать надлежащий код. Об этом многие знают и говорят, но в действительности мало кто заботится или уделяет достаточные внимание и усилия для того, чтобы получить НАДЁЖНЫЙ код.

Рассмотрим несколько примеров ненадлежащего подхода.

Не делайте так: неинициализированные свойства

<?php
class BankAccount
{
    protected $currency = null;
    public function setCurrency($currency) { ... }
    public function payTo(Account $to, $amount)
    {
        // sorry for this silly example
        $this->transaction->process($to, $amount, $this->currency);
    }
}
// I forgot to call $bankAccount->setCurrency('GBP');
$bankAccount->payTo($joe, 100);

В этом случае необходимо помнить, что для проведения платежа следует вызвать сначала setCurrency. Это, действительно, плохой подход — операцию изменения состояния, такую как указанная (проведение платежа), не следует осуществлять в два этапа с использованием двух (или более) публичных методов. Можно иметь много методов проведения платежа, но мы обязаны иметь только один простой публичный метод для изменения статуса (категорически недопустимо нахождение объектов в неопределённом состоянии).

В этом случае мы сделали ещё лучше, инкапсулируя неинициализированное свойство в объект Money:

<?php
class BankAccount
{
    public function payTo(Account $to, Money $money) { ... }
}
$bankAccount->payTo($joe, new Money(100, new Currency('GBP')));

Защитите программу от неправильного обращения. Не используйте неинициализированные свойства объекта.

Не делайте так: дающее утечку состояние за пределами области действия класса

<?php
class Message
{
    protected $content;
    public function setContent($content)
    {
        $this->content = $content;
    }
}
class Mailer
{
    protected $message;
    public function __construct(Message $message)
    {
        $this->message = $message;
    }
    public function sendMessage(){
        var_dump($this->message);
    }
}
$message = new Message();
$message->setContent("bob message");
$joeMailer = new Mailer($message);
$message->setContent("joe message");
$bobMailer = new Mailer($message);
$joeMailer->sendMessage();
$bobMailer->sendMessage();

В этом случае Message (Сообщение) передаётся по ссылке, и результат будет в обоих случаях «joe message» («сообщение Джо»). Решением могло бы быть клонирование объекта сообщения в конструкторе Mailer. Но что мы всегда должны стараться сделать, так это использовать (неизменяемый) объект-значение вместо простого изменяемого объекта Message. Используйте неизменяемые объекты, когда вы можете.

<?php
class Message
{
    protected $content;
    public function __construct($content)
    {
        $this->content = $content;
    }
}
class Mailer
{
    protected $message;
    public function __construct(Message $message)
    {
        $this->message = $message;
    }
    public function sendMessage(
    {
        var_dump($this->message);
    }
}
$joeMailer = new Mailer(new Message("bob message"));
$bobMailer = new Mailer(new Message("joe message"));
$joeMailer->sendMessage();
$bobMailer->sendMessage();

Пишите тесты


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

Выводы


Надеюсь, вам понравилась статья. Помните, что здесь — только предложения. Вам решать, когда и где их применять и применять ли вообще.

Спасибо за прочтение!
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 51

  • НЛО прилетело и опубликовало эту надпись здесь
      +2
      А почему бы и нет?
        +2
        Что не так?
        • НЛО прилетело и опубликовало эту надпись здесь
            +1
            А вы на PHP профессионально работали, или так, видели отрывки кода на форумах? PHP абсолютно никак не мешает строить защищённый сайт. И скриптовая сущность тут вообще ни при чём, тут играет роль исключительно разумность программиста. Решето можно написать и на java и на c#. Пойдите поломайте ВК или фейсбук, раз на то пошло. Они хоть и на своеобразном но на PHP. Да хоть хабр сломайте, в конце-концов, он же тоже на PHP!
              +1
              Любой интерпретатор написан на компилируемом яп, если получится создать такую ситуацию, что где-то при интерпретации происходит переполнение буфера — <цензура типа> всему интернету.

              Взять хотя бы это: CVE-2017-5340 или просто посмотреть статистику.



              Насчет защищенного сайта вы правы, да. Это довольно хороший яп, но когда речь идет о стратегически важных объектах он неприемлим. А в статье именно говорится о таких вещах как ракеты, ага, давайте на php devel studio их писать. Чет не очень.

                +2
                А в статье именно говорится о таких вещах как ракеты

                Если разрабатывающие ракеты учатся по таким статьям, то мне уже страшно.
                  +2
                  К счастью, нет.

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

                  Страшно, когда в мобильных пром. роботах это просто дипломат с ноутом и роутером внутри, с подключенными по COM-порту моторами.

                  Страшно, когда в автомобиле встроенный компьютер — десктопная ОС в режиме киоска.

                  Страшно, когда преподы в универе допускают ошибки типа переполнения буфера и целого.

                  Страшно, когда вообще смотришь на индустрию.

                  Мне — страшно. Лет через 5 будет вочдогс.
                    +1

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

                      0

                      Банкоматы почти все на Windows ;-)

                        +1

                        не почти, а все и причем на XP и Семерке у сбера точно)

                          0
                          Зато у постаматов они на восьмёрке.
                            0
                            Там скорее POSReady, а не XP. Хотя кто их знает.
                      0
                      Писать ракеты на php — согласен, бред (я и саму devel studio бредом считаю). Для таких сложных и важных мест скриптовые языки не годятся. Но для обычных сайтов — php очень и очень неплох. И по собственному опыту, люди которые это отрицают — либо работали на php лет 10 назад, либо и вовсе знакомы с ним только по печально известной статье «фрактал плохого дизайна».
                        0
                        надо было назвать Devil Studio
                        +1
                        просто посмотреть статистику.

                        Законы ненадёжности Джилба


                        1. Компьютеры ненадежны, но люди еще ненадежнее.
                          • В происхождении каждой ошибки, в которой винили компьютер, вы найдёте как минимум две человеческих ошибки, одна из которых — обвинение компьютера в ошибке.
                        2. Любая система, зависящая от человеческой надежности, ненадежна.
                        3. Единственная разница между дураком и преступником, атакующим систему, — в том, что дурак атакует непредсказуемо и по широкому фронту.
                        4. Система имеет тенденцию скорее усложняться, нежели упрощаться, до тех пор, пока итоговая ненадёжность не станет неприемлемой.
                        5. Система самопроверки стремится к усложнению пропорционально собственной ненадёжности системы, в которой она используется.
                        6. Способность системы к поиску ошибок и коррекции может служить ключом к пониманию ошибок, которые не могут быть предсказаны.
                        7. Все реальные программы содержат ошибки, пока не доказано иное, что невозможно.
                        8. Число ошибок, которые нельзя обнаружить, бесконечно, в противовес числу ошибок, которые можно обнаружить — второе конечно по определению.
                        9. В поиски повышения надежности будут вкладываться средства до тех пор, пока они не превысят величину убытков от неизбежных ошибок или пока кто-нибудь не потребует, чтобы была сделана хоть какая-то полезная работа.
                    • НЛО прилетело и опубликовало эту надпись здесь
                        +5
                        Пойдите поломайте ВК или фейсбук, раз на то пошло.

                        Довод так себе… Их, в принципе, регулярно ломают, даже bounty-программы соответствующие есть.

                          0
                          Решето можно написать и на

                          … А у вас негров линчуют!

                          Пойдите поломайте ВК или фейсбук

                          Оба не на PHP.
                            0
                            Оба не на PHP.

                            Обе именно на php. Не на чистом, да, а переписанным под свои нужды, но дизайн языка они не меняли. У вк так вообще неполноценная версия php получилась, без поддержки ооп.
                              +2
                              Так может они для того и обрезали возможности, чтобы пришлось писать менее кривой и более безопасный код?
                    +21
                    Ожидал функционального программирования / контрактного программирования, формальной спецификации и верификации программ, детального юнит-тестирования.
                    По факту — пара советов из начала любой книги о «правильном и красивом» программировании.
                      +1

                      Раз уж вы заговорили. Как вы делите ответсвенность между контрактами и юнит тестами?

                        +1
                        К сожалению, я не пишу и не писал код с контрактами. Не сложилось.

                        А как стал бы использовать — контракты на отлавливание пограничных ситуаций вида
                        1) выбрасывание исключений
                        2) возврат/не возврат null
                        3) ограничение области определения закодированной функции до некоторого реального подмножества.
                        (Пример — не может в вашей модели сепаратор вращаться со скоростью >1кк оборотов в минуту.
                        Ну и отсеките нереалистичные цифры контрактом)

                        «u. тесты — верификация того, что ты написал то, что собирался написать, а не то, что получилось»
                        т.е. обязательно — happy path, ветвления программы, циклы. (дешево)
                        крайне желательно — таблицу истинности всех логических предикатов (дорого)
                        параллельное программирование — корректность состояния, действия при тупиках и гонках. (очень дорого)

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

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

                        P.s. это моё мнение, оно явно подлежит дискуссии, но холивары на эту тему мне уже настолько надоели, что вызывают тошноту.
                        Используйте любой метод доказывания корректности программы, который может сработать автоматически. Чем раньше будет отловлена ошибка — тем лучше.
                        Если вы не используете никаких методов — начните использовать любой, вызывающий у вас меньше всего неприятных эмоций.
                        Не надо холиварить, какие тесты лучше — функциональные, юнит или регрессионные, сначала напишите любой, позволяющий доказать корректность и соответствие требованиям.
                          +1
                          Как пример — почтовый ящик процесса в Erlang.
                          Как ни крути, это область памяти процесса, в которую возможна многопоточная вставка

                          Там как раз однопоточная вставка и сразу проблемы с race condition уходят как класс. Или Вы имеете в виду детали реализации виртуальной машины?

                            0
                            Детали реализации виртуальной машины.
                            В самом эрланге к механизму потоков доступ не получишь, т.к. еденица исполнения — процесс внутри вм.
                        +1

                        причём книги для начинающих

                        +15
                        Забавно, громкое такое название, громкие примеры с ракетами, а дальше код на пхп. Я ничего особого против пхп не имею, но это выглядит как-то смешно.
                          –1

                          в конторе, где работаю, все на пыхе… и 1с...

                            +1
                            Надеюсь, ваша контора не запускает ракеты?
                              0
                              Надеюсь, ваша контора не запускает ракеты?


                              Вряд ли. Но возможно она их делает.
                          0
                          На мой взгляд, здесь правильней было бы рассказать о стандартах программирования для низкоуровневых языков, на которых и реализованы ответственные системы. Например, какие компании какие стандарты используют, как происходит верификация, тестирование и приемка таких программ. А в данной статье просто стандартные рекомендации для минимизации «выстрела себе в ногу».
                            +3
                            «Не изобретайте велосипед» на этом месте защитное программирование кончилось
                              +10
                              Дико извиняюсь за дикий оффтоп и возгласы «все пропало». Но доколе это будет? Все больше трешевых материалов приходит на хабр — то описание из pip вынесли в целую статью, то как нужно писать MVC на php сегодня появилось, теперь это. Раньше такого не было. У меня давно пылиться немного материалов для хабра, но никак не решаюсь опубликовать их ибо каждый раз думаю, что это не уровень для хабра, но сейчас вижу, что можно.
                                +1
                                Я всегда считал, что defensive programming − это что-то вроде антипаттерна. Типа, как испортить читаемость кода и превратить поддержку в ад, предусматривая каждую исключительную ситуацию или перебирая все коды ошибок.
                                  +4

                                  То, что вы описали — JSDD == Job Safety Driven Development :)

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

                                      До первого случая потери денег > твоей зарплаты.
                                      Или до первой человеческой жизни.
                                      +1
                                      Интересненько, как это совместимо:
                                      мы как разработчики должны не доверять коду других разработчиков

                                      можно легко использовать то, что уже сделано


                                        +1
                                        «Не изобретайте велосипед» как-то не вяжется с «Не доверяйте разработчикам».
                                          +1
                                            0
                                            получить 100% покрытие тестами кода.
                                            зачем? coverage это просто цифра, которая ни чего не говорит о качестве тестов. в пределе, 100% покрытие можно легко получить, написав кучу тестов без единого ассерта.
                                              +2

                                              "Не делайте так": написание модуля обработки банковских операций на php.


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

                                                0
                                                В моей практике подобные ошибки (не такие эпические, пока еще никто не умер вроде, но реальный невосполнимый ущерб здоровью людей случался) чаще всего возникали из-за опечаток, которые не всегда заметны на code review. И спасает более формальный подход к написанию ТЗ и по ТЗ — приемочных тестовых сценариев и статический анализ кода. А призывы быть повнимательнее не особо спасают.
                                                +2
                                                Прочитав примеры критических уязвимостей в начале статьи, ожидал программирования, связанного с железом: микроконтроллеры, обработка сигналов, регистры и память, RTOS…
                                                А тут PHP и фреймворки (которые каждый день новые, не успеваешь название выучить)
                                                  0
                                                  У меня все более явное чувство, что статьи все чаще начинают повторяться. Про ариан и рентген уже точно было.
                                                    0
                                                    Помимо багов реализации еще бывают, как минимум, криптографические грехи и сетевые грехи, о которых тоже не плохо было бы упомянуть.
                                                    По поводу юнит тестов замечание интересное, но недостаточное. Юнит тесты должны быть написаны в контексте безопасности, т.е. тесты которые проверяют, что не аутентифицированному пользователю не досталась роль админа, при ошибке например.
                                                    А вообще помимо тестов не плохо практиковать Security Development Lifecycle, в котором, помимо юнит тестов, много чего еще есть.
                                                      0
                                                      По поводу Message и Mailer пришлось перечитать три раза, прежде чем я въехал. Слава C++ и указателям! :)
                                                      Если честно, упоминание php и ракет вызывает когнитивный диссонанс. А тут еще и фреймворки. Как раз если писать ПО для ракеты, широко распространенные фреймворки использовать нельзя ни в коем случае. Тут лучше подходит самопальный «бронированный» велосипед, хотя это и дороже. Но в таких случаях на спичках не экономят.
                                                        +1
                                                        Полёт №501 ракеты «Ариан-5» Европейского космического агентства был прекращён через 40 секунд после старта (4 июня 1996 г.). Экспериментальная ракета-прототип стоимостью 1 млрд. долларов США самоликвидировалась из-за ошибки в бортовом ПО управления.

                                                        Не самый удачный пример для этой статьи. В ПО для "Ариан-5" уже были использованы все возможные техники написания корректного кода, включая, разумеется, многократный ручной и автоматизированный code review. Для таких мегапроектов нужно использовать математическую верификацию кода (и железа), причём на всех уровнях (компьютер, как известно, имеет уровни транзисторов, вентилей, архитектуры процессора, ОС, user space машинного кода и кода на высокоуровневом языке программирования), причём так, чтобы математические доказательства правильности разных уровней были связаны между собой. Вот тогда действительно логических ошибок в компьютере удастся избежать (с некоторыми оговорками). Сошлюсь на свой же коммент на Geektimes для тех, кому интересно: https://geektimes.ru/post/282234/#comment_9672390, я ведь такой умняшка :)

                                                          0
                                                          Минимум 2/3 проблем решается выбором правильного языка.

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

                                                          Самое читаемое