ч1гл5. Запасы по прочности ПО. Ошибки

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

В данном случае применяли несколько разных способов:
1. Прежде всего, организационный - уронил, скажи. Не накажем и даже плохо о тебе не подумаем.
2. Конструкция должна быть легко обслуживаемой: человек должен быть в удобном положении
3. Применение защёлок и конструкций, которые очень трудно уронить, потому что они легко пристёгиваются.
4. Применение мягких материалов в посадках (деталях, контактирующих друг с другом), таких как серебро, которое вместо того, чтобы срезать стружку с другой детали, может просто деформироваться.
5. Применение более точных допусков в посадках для увеличения зазоров (больше зазор между деталями - меньше вероятность того, что детали столкнутся)

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

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

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

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


В этом смысле, в программном обеспечении очень важны аналоги таких сеток, фильтров и огнестойких ограждений.
Представьте себе, что ваше ПО выполняет довольно сложный математический расчёт. Проверить его по визуальным результатам довольно тяжело. Что нужно сделать?
Исходить из предположения, что расчёт вы выполняете неверно. Причём не всегда, а только на некоторых неизвестных вам типах задач. Так, что при тестировании ошибку выявить не удалось.
А значит, необходимо уже при коммерческой эксплуатации проверять в расчётах как промежуточные результаты, так и окончательный результат расчёта. Чтобы недопустить применения ошибочных результатов на практике и показать пользователю, что здесь программа рассчитала всё неверно.

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

Хорошая проверка промежуточных результатов на каждом шаге может очень сильно упростить локализацию и исправление ошибок. Так как сам этап возникновения ошибки вам будет известен, вы, возможно, сразу же вспомните алгоритм и, возможно, сразу догадаетесь, что именно идёт не так. Если же этап не известен, искать ошибку можно очень долго. Иногда годами (если ошибка не повторяется, например, ошибка синхронизации в многопоточном алгоритме). При этом находятся другие ошибки рядом, но та самая - остаётся.


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

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


Таким образом, один из основных принципов проектирования - это принцип "в каждой функции/модуле - ошибка". Мы сразу же закладываемся на то, что у нас есть ошибки.


Обсудим, какие существуют приёмы обеспечения информационной безопасности ПО, являющиеся реализацией принципа "в каждом модуле ошибка".

Давайте посмотрим на примере частой для веб- и ftp-серверов ошибки траверса директории ("обхода папки", как её ещё называют). Ошибка заключается в том, что у сервера, отдающего файл из директории сайта, можно запросить файлы и из других директорий, так как сервер обращается к функциям операционной системы, которые понимают синтаксис траверса директории "/../".
Пусть директория сайта "C:/http", а секретный документ расположен по пути "C:/secrets/secret.html". Обычный сетевой запрос к файлу "C:/http/nosecrets.html" выглядит как "nosecrets.html". Но можно запросить файл "C:/secrets/secret.html" следующим способом: "/../secrets/secret.html". Ошибка может повлечь за собой кражу важных данных с сервера.

Допустим, мы забыли про возможность траверса директории.
Как можно предотвратить данную атаку приёмами программирования на основе принципа "в каждом модуле - ошибка"?

Прежде всего, можно использовать очень частный приём в виде "запрет по умолчанию".

В данном случае, это может быть разграничение по белому списку. То есть сервер при своём запуске осуществляет полное сканирование директорий, к которым разрешён доступ пользователям.
Он составляет белый список полных путей файлов, к которым разрешён доступ. Потом удаляет из них "C:/http".

Допустим, в директории "C:/http" есть один файл "nosecrets.html" и нет других разрешённых файлов и директорий. Тогда белый список будет содержать один путь "nosecrets.html".

При поступлении запроса, сервер сравнивает путь из запроса с белым списком. Запрос вида "/../secrets/secret.html" не совпадает ни с одним элементом в белом списке, следовательно будет запрещён.
Также будет запрещён "nosecrets.html:$DATA". Это важная штука, если мы работаем с системой NTFS, так как это обращение к файловому потоку.
Представим себе, что наш сервер обрабатывает интерпретатором все файлы по путям, заканчивающимся на ".html". То есть файл "nosecrets.html" содержит код программы, которая уже даёт на выходе интересный пользователю текст. Однако сам код программы должен быть недоступен пользователю.
Путь "nosecrets.html:$DATA" не заканчивается на ".html", однако является корректным путём файла в системе NTFS. Значит, сервер не будет обрабатывать данный файл интерпретатором, но, в то же время, выдаст нам его содержимое, так как "nosecrets.html:$DATA" для NTFS является синонимом "nosecrets.html". Как мы видим, белый список заблокирует и эту атаку.

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

То есть вместо логики программы в виде "разрешение по умолчанию"
если это .html, запустить интерпретатор и выдать результат его работы в сети
если это другое - выдать в сеть запрошенный файл // здесь мы разрешили для всех неизвестных вариантов некоторое опасное действие

мы имеем логику программы, например, в виде
если это .html, запустить интерпретатор и выдать результат его работы в сети
если это .jpg выдать запрошенный файл в сеть
если это другое - выдать ошибку "Доступ запрещён" // здесь мы запретили опасное действие, если не знаем, что оно безопасно


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


Ещё один пример приёма "запрет/блокирование по умолчанию" можно привести на примере такого абстрактного алгоритма.
Если пользовать из группы notauthenticatedUser, запретить доступ на чтение
Если пользователь из группы authenticatedUser, разрешить доступ на чтение
Иначе разрешить доступ на чтение и запись

Этот алгоритм неверен тем, что по умолчанию разрешает чтение и запись. Он предполагает наличие только трёх групп пользователей: notauthenticatedUser, authenticatedUser и AdminUser.
Он хорошо работает, пока это предположение выполнено. Но как только появляется новая группа, например, restrictedUser, она тоже получит доступ по умолчанию. А это самый высокий уровень доступа.

Должно быть всё наоборот:
Если это AdminUser - разрешить доступ на чтение и запись
Если пользователь из группы authenticatedUser, разрешить доступ на чтение
Иначе запретить доступ на чтение


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

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


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


Как ещё можно запретить пользователю доступ, не зная об ошибке траверса директории?

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

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

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

Путь "/../secrets/secret.jpg" первый процесс не передаст второму, так как это статический файл изображения. Он попытается его получить сам, однако такая активность первого процесса будет заблокирован операционной системой (мы запретили доступ к секретным файлам первому процессу). Значит здесь мы не дали хакеру воспользоваться ошибкой траверса директории.
Разумеется, такие заблокированные запросы нужно немедленно логировать и анализировать. Затем исправлять ошибки.

При ошибке траверса директории при запросе "/../secrets/secret.html" первый процесс передаст запрос второму. Второй процесс здесь должен проверить, что файл допустимо запускать. Если не проверит, то он его запустит. Это может быть очень серьёзной ошибкой.
Чтобы этого избежать, второй процесс не должен связывать свои функции с файловыми путями и файлами, содержащими программный код.
Он должен принимать от первого процесса команды по специальному протоколу взаимодействия этих двух процессов, в которых невозможен траверс директории.

Таким образом, здесь важно упрощение системы в виде отказа от использования функциональности файловой системы. Хотя это усложнит код в том смысле, что нам придётся писать обработчик команд, это упростит код в том смысле что:
1. Исходные коды обработчика команд есть у нас, мы его можем изучить, если надо, досконально
2. Обработчик команд проще по функциям, чем файловая система, поэтому нам надо учитывать меньше вариантов поведения

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

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


Разумеется, я здесь должен пояснить, что я рассматриваю простую ошибку, которую просто исправить, как пример того, как организовывать многоэшелонированную оборону (оборону в глубину). И да, ошибку, после выявления, надо всегда исправлять - она всё равно остаётся ошибкой и хакер может придумать, как ей воспользоваться, даже если нам ничего в голову путного не приходит.
Что ещё можно сделать, чтобы сделать ПО устойчивым к наличию ошибки траверса директории?


Например, можно удалить все ненужные файлы и вообще другие сервисы с компьютера. Например, мы говорили о файлах "/../secrets/secret.html" и "/../secrets/secret.jpg". Они не должны быть доступны пользователю веб-сервиса. Но тогда зачем они вообще нужны на этом компьютере?

Либо их используют/вызывают какие-то другие скрипты, либо они не нужны. Во втором случае их надо удалить с компьютера. То есть упростить систему. Так, часто разносятся на разные аппаратные серверы разные сетевые сервисы. Отдельно DNS, отдельно почтовый сервис, отдельно стоит внешний веб-портал, отдельно корпоративный портал.
Если в каком-то из них найдётся ошибка, хакер взломает только их, но не взломает остальные серверы, так как они находятся на других аппаратных серверах.

Допустим файл "/../secrets/secret.bd" нужен только для работы скрипта "/../secrets/secret.html". Тогда этот файл и скрипт можно отделить от других скриптов на отдельную аппаратную часть. Обратный прокси будет перенаправлять запросы пользователя к отдельному серверу, который будет запускать только файл "/../secrets/secret.html" и иметь очень ограниченную функциональность. Другие скрипты будут располагаться на других серверах и не смогут по ошибке обратиться к "/../secrets/secret.bd". Решение, в целом, аналогично разделению приложения на процессы с разницей в виде того, что разделение происходит на разные аппаратные серверы.


Далее. Файловый путь можно приводить к каноническому виду. То есть к виду, не допускающему двусмысленность.
Это означает, что мы точно знаем кодировку строки (например, UTF-8) и точно знаем путь к файлу безо всяких условностей. Так, путь "/../secrets/secret.html" должен быть преобразован к "C:/secrets/secret.html". Тогда сервер сможет распознать, что запрашиваемые файл не из разрешённой директории.
Однако, для этого нужно знать, что строку вообще можно привести к каноническому виду. Зато это поможет и для сервера, и для архиватора.


Кроме этого, можно использовать фильтрацию по шаблону. Фильтр безопасности будет блокировать все пути, не подходящие под шаблон.
Например, алгоритм может быть таков:
строка не начинается со слеша
разделить строку по слешам, записать полученный результат (массив строк) в переменную splitted
последняя строка массива - это имя файла. Остальные - имена директорий
Имена директорий содержат только латинские символы и цифры нижнего регистра. Не менее 1 символа, не более 32 (тут можно вставить другие ограничения).
Имя файла содержит одну и только одну точку. Точка делит файл на две части. Первая часть того же шаблона, как имя директории. Вторая часть не более 4-х символов, остальное такое же, как имя директории.

Такая фильтрация, к сожалению, заставит пользователя думать о том, как называть директории и ограничит пользователя в выборе удобных для него наименований. Поэтому это не очень хорошо. Однако, при запросе к файлу "/../secrets/secret.html" алгоритм фильтрации выдал бы ошибку: точки в именах директории запрещены.

Фильтрация плоха ещё и тем, что если мы разрешим точки, то она пропустит данный запрос как корректный. А пользователям, возможно, всё равно оставит проблемы. Поэтому её стоит использовать только в особых случаях.

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


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


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