Просмотров: 3591

Kung-Fu Nemerle или общая теория безопасного кода. Часть 2


Продолжаем наш ранее начатый разговор о специфическом языке Nemerle и общей теории безопасного программирования.

Сегодня мы рассмотрим ситуации с SQL injections и Race Conditions, подробно обсудим контроль данных (валидация и экранирование), ну и опять же очень много будем говорить о DbC в современном программировании.


Nemerle безопасное программирование код уязвимости

Внедрение кода SQL aka SQLi

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

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

Разумеется, использование ORM-фреймворков и LINQ решает проблему SQLi весьма кардинальным образом, но, тем не менее, в случае если разработчику понадобится сформировать SQL-запрос врукопашную, ничто не помешает ему воспользоваться той же форматной строкой, чтобы записать всю конструкцию в одну текстовую строку:

// C#
string cmdText=string.Format("SELECT * FROM Customers 
 WHERE Country='{0}'", countryName);
SqlCommand cmd = new SqlCommand(cmdText, conn);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
  Console.WriteLine(String.Format("Customer: {0} {1}", 
 reader[0], reader[1]));

вместо написания лишних строк кода, определяющих параметры запроса:

// C#
string commandText = "SELECT * FROM Customers WHERE 
 Country=@CountryName";
SqlCommand cmd = new SqlCommand(commandText, conn);
cmd.Parameters.Add("@CountryName",countryName);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
  Console.WriteLine(String.Format("Customer: {0} {1}", 
 reader[0], reader[1]));

Думаю, понятно, что будет при выполнении такого запроса, если атакующий сможет передать в countryName строку типа from Russia with love'; DRОP TАBLE Customers — в первом случае?

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

// Nemerle
ExecuteReaderLoop("SELECT * FROM Customers WHERE 
 Country=$countryName", dbcon, 
{
  WriteLine($"Customer: $firstname $lastname\n")
})

И здесь также нет никакой магии и поддержки со стороны ядра языка. ExecuteReaderLoop является макросом, который во время компиляции разворачивает эту конструкцию в аналогичную приведенной во втором примере C#-кода, переписывая ее в код создания корректно параметризованного объектами SQL-запроса (типы параметров которого определяются по типам $-переменных, указанных в тексте запроса), его исполнения и итерации по списку полученных записей.

Помимо макроса ExecuteReaderLoop в пространстве имен Nemerle.Data определены также макросы: ExecuteScalar для обработки скалярных запросов, возвращающих единственное значение и ExecuteNonQuery для исполнения запросов, не возвращающих ничего.

Как мы видим, в случае с SQLi, средства Nemerle дают уже весьма ощутимое преимущество перед другими языками, предоставляя разработчику возможность формировать SQL-запросы действительно удобным, но в то же время безопасным образом.

Гонки aka Race Conditions

Не секрет, что гонки возникают там, где имеют место побочные эффекты. Также не секрет, что побочные эффекты имеют место там, где используются изменяемые переменные или поля классов. И уж совсем не секрет, что изменяемые переменные или поля классов используются в том же C# повсеместно, да и вообще-то являются там таковыми по умолчанию.

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

Nemerle безопасное программирование код уязвимости

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

А вот для объявления изменяемых сущностей, необходимо и в том и в другом случае использовать ключевое слово mutable , которое более чем в два раза длиннее слова def ! Разумеется, ленивый разработчик будет использовать def , где только можно, а поля класса объявлять изменяемыми только там, где это действительно необходимо.

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

Но самое страшное, что разрабатывая после этого на C#, такой разработчик чувствует постоянный осадочек из-за того, что он мог забыть где-то воткнуть readonly или const (превозмогая лень, заметьте!), одновременно осознавая, насколько этого недостаточно, чтобы получить те же гарантии, которые он имел в Nemerle, совершенно не напрягаясь.

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

Но сам стиль кодирования на Nemerle, диктуемый дизайном языка, склоняет пишущего код к тому, что бы он постоянно задавался вопросом — а действительно ли объявление изменяемой сущности в каждом конкретном месте — это именно то, что нужно для решения текущей задачи?

И это реально работает.

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

Computation Expressions заимствована из F# и предоставляет в распоряжение разработчика набор так называемых «билдеров», позволяющих комбинировать вычисления тем или иным способом. В числе билдеров, уже реализованных в библиотеке, присутствует билдер Async , предназначенный для реализации многопоточных вычислений.

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

Контроль данных

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

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

Nemerle безопасное программирование код уязвимости

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

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

Экранирование же, напротив, предназначено для преобразования либо упаковки данных в формат, достаточный для обеспечения их корректной обработки и осуществляется по месту непосредственного использования этих данных тем или иным образом. Задача экранирования вряд ли может иметь обобщенное, универсальное решение на все случаи жизни, тем более, поддерживаемое средствами языка. Хотя бы, потому что логика экранирования сильно зависит от того, где мы планируем использовать эти данные. Это достаточно хорошо видно на приведенных выше примерах экранирования данных для HTML и SQL (да-да, объектная параметризация SQL-запросов тоже является одним из способов экранирования, хотя и не изменяющего данные, но передающего его во внешнюю среду безопасным для нее образом).

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

Задача валидации же, напротив — вполне формализуема и поддается обобщению. Одним из таких обобщений является контрактное программирование aka Design by Contract (DbC). Поддержка данной концепции была реализована в .NET 4 и о том, что она из себя представляет, как выглядит C#-код, реализующий контракты и какие шаги необходимо проделать разработчику, чтобы обеспечить поддержку контрактов в своем C#-проекте, можно подсмотреть в статье Романа Калита «Code Contracts в .NET 4.0».

Между тем, поддержка контрактного программирования в Nemerle появилась еще задолго до выхода .NET 4.0 и мало чем отличается от реализации Microsoft в плане предоставляемых возможностей (по крайней мере в части, касающейся контроля соблюдения контрактов в runtime), но имеет и ощутимое преимущество, заключающееся в том, что реализация DbC в Nemerle позволяет декларативно описывать контракты, органично вписываясь в основной синтаксис языка.

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

Давайте рассмотрим, что нам может дать поддержка DbC в Nemerle в плане валидации данных на простеньком примере. Допустим, перед нами стоит задача получить от пользователя его имя и URL его домашней страницы и вернуть ему HTML-документ с приветствием и ссылкой. Реализуем метод, принимающий на вход две строки (допустим, что они выбираются прямехонько из параметров HTTP-запроса и никак не обрабатываются до попадания в наш метод) и возвращающий сформированный текст документа, который необходимо передать в ответ на запрос:

// Nemerle
DoJob(name : string, page : string) : string
{
  (xml
<#<html>
  <head>
    <title>
        User page
    </title>
  </head>
  <body>
    Name: $name
    <a href=$page>Homepage</a>
  </body>
</html>#>
   ).ToString()
}

Обратите внимание, что аргумент page попадает внутрь тега <а> и средств экранирования, предоставляемых нам XML-литералом уже недостаточно для того, чтобы избежать угрозы реализации XSS (хотя бы, потому что атакующий может передать в этом параметре строку типа javascript:аlert(0); или использующую URI-схему data: и выполнить этот код в браузере жертвы при щелчке по такой ссылке), поэтому сформулируем более жесткие требования к содержимому аргументов рассматриваемого метода.

Пусть name может содержать в себе только латинские буквы, цифры и символ подчеркивания и длина этого аргумента должна лежать в диапазоне 5-20 символов. Пусть page должен быть правильно сформированным URL, использующим схему HTTP.

Опишем эти требования с помощью средств DbC:

// Nemerle
public static IsValidUserName(this s : string) : bool
{
  Regex(@"^[A-Za-z0-9_]{5,20}$").IsMatch(s)
}
public static IsValidHttpUri(this s : string) : bool
{
  Uri.IsWellFormedUriString(s, UriKind.Absolute) &&
  s.StartsWith("http://")
}
DoJob(name : string, page : string) : string
requires
  name.IsValidUserName() && page.IsValidHttpUri()
{
  // ... - код XML-литерала из предыдущего примера
}

Здесь requires - это макрос, определенный в пространстве имен Nemerle.Assertions стандартной макробиблиотеки, позволяющий определить предусловия метода в терминах DbC и добиться результата, который мы ожидали. Разумеется, особой необходимости выносить реализацию условий в отдельные методы нет, но это позволяет разгрузить синтаксис определения условия и собрать в одном месте реализацию всей логики валидации.

И раз уж мы ударились в паранойю, то нужно биться до конца. Что, если в реализации экранирования XElement , или методе IsWellFormedUriString , и в нашем регулярном выражении обнаружатся ошибки, которые позволят атакующему обойти все наши защитные механизмы, передать в наш документ malformed-строку и, тем самым, нарушить его структуру (то есть заставить XElement.ToString() вернуть не валидный XML-документ)? Что, если другие разработчики проекта реализуют в этом методе код, который изменяет уже преобразованный в строку XElement и сводит на нет все наши усилия по экранированию?

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

// Nemerle
// ... - методы-расширения из предыдущего примера
 
public static IsValidXmlSyntax(this s : string) : bool
{
  try {
    XElement.Parse(s) != null
  } catch {
    | _ is System.Xml.XmlException => false
  }
}
 
DoJob(name : string, page : string) : string
requires
  name.IsValidUserName() && page.IsValidHttpUri()
ensures 
  value.IsValidXmlSyntax()
{
  // ... - код XML-литерала из предыдущего примера
}

Здесь мы добавили еще один метод IsValidXmlSyntax() , проверяющий корректность синтаксиса XML в передаваемой ему строке и использовали новый макрос ensures , предназначенный для определения постусловий контракта нашего метода.

Идентификатор value используется для ссылки на значение, возвращаемое методом. Но что произойдет, если будут нарушены пред- или постусловия контракта нашего метода, т.е. если он будет вызван с аргументами, не удовлетворяющими предусловию или попытается вернуть данные, не удовлетворяющие постусловию?

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

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

// Nemerle
 
// ... - методы-расширения из предыдущего примера
 PreConditionFail(name : string, page : string) : (string * string)
{
  WriteLine($"Precondition failed with name='$name' and page='$page'");
  ("Annonymous", "about:blank")
}
 
DoJob(mutable name : string, mutable page : string) : string
requires
  name.IsValidUserName() && page.IsValidHttpUri() 
otherwise 
  (name, page) = PreConditionFail(name, page)
ensures 
  value.IsValidXmlSyntax()
{
  // ... - код XML-литерала из предыдущего примера
}

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

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

// Nemerle
class UserInfo
invariant name.IsValidUserName() && page.IsValidHttpUri()
{
  private mutable name;
  private mutable page;
  public this (name : string, page : string) : void 
  { ...

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

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

И это все?

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

Nemerle безопасное программирование код уязвимости

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

Например, основа библиотеки Nemerle.ComputationExpressions была реализована сторонним разработчиком just for fun в попытке ответить на свой собственный вопрос: «а можно ли так же, как на F#, только в Nemerle?». Теперь вот и мы, в этой заметке, задались вопросом на тему безопасного кода... ;)

В заключение

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

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

Например, возможное дальнейшее развитие идеи XML-литералов в направлении безопасности, заключается в построении на их основе движка шаблонов XML/HTML, интегрированного с библиотекой AntiXSS либо реализующего ее функционал, поскольку между ним и традиционными методами экранирования, применяемыми в System.Xml.Linq (поверх которого и построены XML-литералы), есть одна небольшая, но существенная разница, аналогичная разнице с экранированием при помощи HttpUtility . Подобный движок был бы достойным конкурентом всевозможным Razor’ам по части защищенности от угроз XSS.

Другим интересным направлением была бы реализация концепции, аналогичной taint-check’ам в языке Perl. Учитывая практически неограниченные возможности макросов по анализу кода на этапе компиляции, было бы довольно здорово получить возможность отслеживания «загрязнения» кода недоверенными данными и контролю их очистке, как в compile-, так и в runtime. Продолжением этого направления является реализация возможности подключать в состав проекта, модели угроз, аналогичные TMT’шным и формировать логику контроля данных в коде на основе информации о пересечении ими границ доверия, заданных в моделях.

В репозитории Nemerle есть несколько примеров весьма изящной реализации design-паттернов на базе макроатрибутов. Ничто не мешает создать макробиблиотеку, реализующую некоторые из security-паттернов (смотрите тут и тута), аналогичным образом.

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

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

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

Я могу продолжать бесконечно, на самом деле, поэтому, с вашего позволения, я все же остановлюсь :) Отмечу лишь, что в приведенных выше «хотелках» нет, ровным счетом, ничего фантастического, все это можно реализовать на макросах Nemerle прямо сейчас, было бы кому и было бы когда.

На правах эпилога

Разумеется, многие угрозы остались за бортом, не вписавшись в рамки этого обзора. Открою небольшой секрет, данная заметка была написана по еще весьма сырым наработкам серии статей для журнала RSDN, посвященных разработке защищенного кода на Nemerle, в которых будут охвачены все известные на сегодняшний день «смертные грехи компьютерной безопасности» (смотрите — «24 смертных греха компьютерной безопасности», М. Ховард, Д. Лебланк, Дж. Вьега, 2010 г., ISBN 978-5-49807-747-5, 978-0071626750).

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

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

Начало этой статьи здесь
Базируется на материале Владимира Кочеткова, 2011

twitter.com facebook.com vkontakte.ru odnoklassniki.ru mail.ru ya.ru pikabu.ru blogger.com liveinternet.ru livejournal.ru google.com bobrdobr.ru yandex.ru del.icio.us

Подписка на обновления блога → через RSS, на e-mail, через Twitter
Теги: , , , , ,
Эта запись опубликована: Воскресенье, 29 января 2012 в рубрике Программирование.

Оставьте комментарий!

Не регистрировать/аноним

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

Зарегистрировать/комментатор

Для регистрации укажите свой действующий email и пароль. Связка email-пароль позволяет вам комментировать и редактировать данные в вашем персональном аккаунте, такие как адрес сайта, ник и т.п. (Письмо с активацией придет в ящик, указанный при регистрации)

(обязательно)


⇑ Наверх
⇓ Вниз