Язык программирования Nemerle является весьма привлекательным инструментом, в первую очередь благодаря тому, что предоставляет в распоряжение разработчика достаточно мощные и выразительные средства метапрограммирования, позволяя прикладному разработчику влиять на ход компиляции и, практически произвольным образом, манипулировать объектной моделью компилируемого кода. Эти средства, помимо прочего, позволяют вводить в язык абстракции более высокого уровня, избавляющие программиста от рутины кодирования и приближающие его к программированию в терминах предметной области разрабатываемой информационной системы (ИС).
Но цель и область обсуждения этой статьи куда шире лишь Nemerle как такового. Какие наиболее типичные и часто встречающиеся ошибки существуют ныне в программировании вообще, какова их природа и условиях возникновения? Что если посмотреть на понятие предметной области под несколько иным углом и представить ее, как область написания безопасного кода и разработки ИС, в принципе защищенных от реализации информационных угроз?
Пожалуй, это позволит даже программистам на других популярных языках программирования немножко по-другому взглянуть на свой инструмент разработки, более ясно выявить для себя некоторые паттерны безопасности общие для всех языков в программировании, которые, безусловно, существуют.
Ответам на эти вопросы и посвящена эта статья в двух частях.
«Внедрение безопасности в ваш продукт полностью зависит от вас. И никто другой не решит все проблемы безопасности за вас, никакое волшебное средство или язык программирования. <...> Это можете сделать только вы сами.»
© Майкл Ховард, «8 простых правил для разработки более безопасного кода», Microsoft MSDN
Хочу сразу оговориться и обратить еще раз ваше внимание на эпиграф. Ни один из ныне существующих языков программирования не является той серебряной пулей, которая бы позволила исключить возможность появления в коде ошибок, приводящих к реализации информационных угроз в ИС.
Он и не может ей являться, хотя бы потому, что достаточно большое количество классов уязвимостей обусловлено не ошибками кодирования, а ошибками развертывания ИС или проектирования ее архитектуры.
И в подобных случаях, язык программирования, как средство реализации спроектированной архитектуры и сборки того, что предполагается развертывать, не всегда способен предоставить средства, которые бы послужили контрмерами для уязвимостей, допущенных на этапах, отличных от этапа написания кода.
В то же время, от языка программирования зависит многое. Если язык не препятствует или даже поощряет использование опасных с точки зрения безопасности конструкций кода, если он заставляет программиста постоянно думать о различных мелких, но многочисленных «особенностях», не имеющих отношения к предметной области в которой тот решает текущую задачу, если вместо простой сущности «список клиентов», язык заставляет помнить о сущности «список указателей на объекты, занятую память которыми необходимо освободить при удалении объекта» и т.п., то обеспечение безопасности кода на таком языке потребует массу затрат со стороны программиста, по сравнению с языком, лишенным этих недостатков.
И, если в ряде задач, действительно требующих низкоуровневых средств, просто не остается иного выбора, как засучив рукава, обеспечить эту самую массу затрат, то у разработчиков прикладной области выбор все-таки есть, а если и нет, то это говорит о наличии причин, скорее административного характера, чем технического.
Правильный язык программирования, с точки зрения нашей предметной области, должен не только обеспечивать благоприятное для программистов окружение разработки на нем защищенного от информационных угроз кода, но и предоставлять средства доработки и расширения этого окружения под нужны разработчиков, а также иметь в своем арсенале уже готовые средства, которые помогли бы справляться с наиболее распространенными и часто встречающимися угрозами. И главное достоинство Nemerle в том, что он как раз и является таким языком.
Давайте пройдемся по наиболее типичным и часто встречающимся ошибкам, приводящим к информационным угрозам и посмотрим, какие средства нам предоставляет Nemerle для того, чтобы избежать их. А затем, мы немного поразмышляем над тем, какие средства мы можем создать самостоятельно, благодаря поддержки этим языком парадигмы метапрограммирования.
К великому счастью, уязвимости всевозможных переполнений (буфера, кучи, целочисленных и вещественных типов) теоретически неактуальны для управляемых языков и, казалось бы, нет никакой разницы между такими языками, но это не совсем так.
Добиться переполнения буфера, к примеру, в C#, все же можно, слегка ошибившись в коде с unsafe-блоками, которые позволяют ностальгирующим по C++ получить все его прелести «не выходя из домика».
Вот пример переполнения буфера на C#, взятый отсюда:
// C# public unsafe struct testo { public fixed int items[16]; public int after; } testo x = new testo(); x.after = 1; for (int i = 0; i <= 16; ++i) { unsafe { x.items[i] = 99; } } Console.WriteLine(x.after);
Данный пример выведет на консоль число 99 из-за того, что обращение к несуществующему x.items[16]
фактически перезатрет значение x.after
. Кто-то может возразить, что нужно сильно постараться, чтобы наступить на такие грабли, программируя на C#. На это, я могу лишь заметить, что постараться тут нужно только в плане появления в проекте unsafe-блоков. Как только это произойдет, стараний потребуется не более, чем при разработке на том же C++.
И вот тут то, что некоторые считают недостатком Nemerle с точки зрения разработки, внезапно становится достоинством с точки зрения безопасности: Nemerle не поддерживает unsafe-блоки, а следовательно, сопровождая готовый код на нем, вы не наткнетесь там на подобные скелеты в шкафу.
Впрочем, как сказал Влад Чистяков (лидер проекта по разработке компилятора Nemerle) в ответ на вопрос, поддерживает ли этот язык unsafe: «Нет. Но может использовать intrerop
(PInvoke
и COM-intrerop
). С их помощью, к сожалению тоже можно легко привести программу к неработоспособности», что лишний раз подтверждает тезис об отсутствии теоретической возможности у языков программирования, стать когда-нибудь серебряными пулями безопасности.
Что касается форматных строк, то в Nemerle реализована как поддержка традиционных для .NET форматных строк с блэкджеком со StringBuilder
’ом и плейсхолдерами, так и ряд средств, облегчающих форматирование строк, и являющихся по сути синтаксическим сахаром для традиционного подхода. В первую очередь, это так называемые splice-строки, позволяющие передать в строку значения и даже выражения на манер PHP:
// Nemerle def name = "Vladimir"; WriteLine($"Hello, $name!"); WriteLine($"Goodbye, $(name.ToUpper())!");
Nemerle также поддерживает рекурсивные строки вида:
// Nemerle def name = "Vladimir"; WriteLine( $<#Hello $name! Goodbye $(name.ToUpper())! #> );
Данный пример является абсолютным аналогом предыдущего. Кроме того, в стандартной макробиблиотеке Nemerle, в пространстве имен Nemerle.IO
определен макрос printf
, являющийся аналогом одноименной функции форматного вывода языков C/C++:
// Nemerle printf("Value = %d", 2 + 2) // Выведет на консоль число 4
Преимущество же макроса printf
в Nemerle перед функцией printf
в сях заключается в том, что разворачиваясь на этапе компиляции в тот же StringBuilder
с плейсхолдерами, он, по ходу, обеспечивает строгий контроль соответствия типов и их количества, указанных в форматной строке с фактически переданными аргументами, чем гарантирует отсутствие уязвимостей, характерных для форматных строк в неуправляемых языках.
Инъекции данных в целом и межсайтовый скриптинг (в простонародье — «XSS») в частности, являлись и продолжают являться настоящим бичом современных web-приложений. Об этом красноречиво говорит статистика уязвимых приложений в архивах XSSed.com. О борьбе с подобными инъекциями в ASP.NET и смежных с ним фреймворках написаны тонны полезных статей и нет никаких причин для того, чтобы не воспользоваться ими при разработке приложений под ASP.NET, ASP.NET MVC или NRails. В первую очередь, это официальное руководство по искоренению XSS от Microsoft, и использование библиотеки AntiXSS для более эффективного экранирования данных из недоверенных источников перед помещением их в тело HTML или XML документов.
Но в наборе стандартных библиотек, поставляемых с Nemerle есть еще кое-что, что позволяет немного облегчить жизнь разработчику. Не так уж и редко возникает задача генерации какого-либо фрагмента HTML (или XML, в общем случае) на лету, в runtime, на основе входных данных, полученных от пользователя. Возьмем, к примеру, задачу отображения его анкетных данных, ограничившись для простоты только именем.
В этом случае, C#-разработчик, еще не знающий о необходимости экранирования данных, напишет нечто подобное:
// C# var html = String.Format(@" <html> <head> <title> User page </title> </head> <body> Name: {0} </body> </html> ", name);
где name
- имя, переданное пользователем. И получит HTML-инъекцию во всей красе, которая приводит к межсайтовому скриптингу, поскольку атакующему ничто не мешает передать в качестве своего имени строку
и тем самым заставить сервер возвращать в ответ на запрос данных об этом пользователе следующий документ:
<html> <head> <title> User page </title> </head> <body> Name: <script>alert(document.cookie);</script> </body> </html>
обработка которого браузером повлечет за собой выполнение внедренного javascript-кода.
Все, что должен сделать в данном случае C#-разработчик, чтобы устранить уязвимость — это экранировать переменную name
перед вставкой в шаблон документа вызовом HttpUtility.HtmlEncode(name)
.
HttpUtility
из пространства имен System.Web
.И тут уместны вопросы: а если данных, подставляемых в шаблон — несколько десятков? А если шаблон собирается из нескольких фрагментов, еще и вложенных друг в друга?
Ситуация не такая уж и невероятная, я много раз сталкивался с приложениями, в которых документы собирались именно таким образом, из заинлайненных в код текстовых фрагментов. Вероятность того, что разработчик допустит ошибку и не обеспечит экранирование какой-либо переменной очень высока, а если и обеспечит, то читать потом нагромождения вызовов HtmlEncode()
в попытке разобраться, какая из переменных будет подставлена в форматной строке вместо, скажем {7}
- занятие не из приятных.
Между тем, все, что нужно разработчику, чтобы безопасно решить эту задачу на Nemerle, это написать так:
// Nemerle def html = xml <# <html> <head> <title> User page </title> </head> <body> Name: $name </body> </html> #>
указав в своей сборке референс на макросборку . Nemerle.Xml.Macro
..
В этом случае, если атакующий попытается передать в name строку, приведенную выше, наш код создаст в переменной html документ, чье текстовое представление выглядит так:
<html> <head> <title> User page </title> </head> <body> Name: <script>alert(document.cookie);</script> </body> </html>
... что совершенно идентично использованию HtmlEncode()
. Почему? Потому что xml <#...#>
- это макрос из подключенной нами макробиблиотеки, обеспечивающий поддержку так называемых XML-литералов, аналогичных реализованным в языках Scala и Visual Basic.
На первый взгляд, может показаться, что этот макрос реализует возможность встраивания фрагментов XML непосредственно в код, оперируя их текстовым представлением, но это не так. На самом деле, он является синтаксическим сахаром для построения XElement-дерева — объектной модели
Окончание этой статьи читайте здесь.