Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Framework »

Обсудить на форуме Обсудить на форуме

Принципы безопасного кодирования для .NET Framework
Общеязыковая исполняющая среда (common language runtime, CLR) и Microsoft .NET Framework предоставляет всем приложениям с управляемым кодом защиту на основе признаков (evidence-based security). В большинстве случаев при написании кода обеспечивать защиту явным образом не требуется. В этом документе кратко описывается система защиты, рассматриваются вопросы безопасности, которые вам, возможно, понадобится учитывать при написании кода, и излагаются принципы классификации компонентов, позволяющие определять, что нужно предпринять для гарантированной защиты кода.
Аннотация - Общеязыковая исполняющая среда (common language runtime, CLR) и Microsoft .NET Framework предоставляет всем приложениям с управляемым кодом защиту на основе признаков (evidence-based security). В большинстве случаев при написании кода обеспечивать защиту явным образом не требуется. В этом документе кратко описывается система защиты, рассматриваются вопросы безопасности, которые вам, возможно, понадобится учитывать при написании кода, и излагаются принципы классификации компонентов, позволяющие определять, что нужно предпринять для гарантированной защиты кода.

Квалификация, необходимая для понимания данных материалов - У читателей должен быть опыт работы с CLR и Microsoft .NET Framework, в том числе базовые знания о защите на основе признаков и по правам доступа кода (code access security).

Защита на основе признаков и по правам доступа кода

Для защиты управляемого кода используются две технологии:

 

  • защита на основе признаков (evidence-based security) - позволяет определять, какие разрешения следует предоставлять коду;
  • защита по правам доступа кода (code access security) - позволяет проверять, весь ли код в стеке имеет необходимые разрешения на выполнение каких-либо действий.

Эти две технологии связаны между собой через концепцию разрешений. Разрешение (permission) - это право на выполнение определенной операции, подлежащей защите. Например, "читать из c:\temp" относится к файловому разрешению, а "подключаться к www.msn.com" - к сетевому.

Защита на основе признаков определяет разрешения, выдаваемые коду. Признак (evidence) - это информация, присущая любой сборке (разрешения предоставляются на уровне сборок) и используемая в качестве входных данных для политики безопасности (security policy). По признакам и политике безопасности, устанавливаемой администратором, система защиты определяет, какие разрешения могут быть выданы коду. Программа сама может запрашивать какое-либо разрешение, влияя на состав окончательного набора разрешений. Запрос разрешения выражается в виде объявления на уровне сборки с синтаксисом пользовательских (custom) атрибутов. Однако в любом случае код не может получить более широкие или ограниченные разрешения, чем это предписано политикой безопасности. Разрешение предоставляется только раз и определяет права всего кода в сборке. Для просмотра и изменения политики безопасности используется инструмент настройки .NET Framework (Mscorcfg.msc).

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

 

Характеристика Описание
Hash Хэш сборки
Publisher Лицо или организация, использующее подпись AuthentiCode
StrongName Открытый ключ+имя+версия
Site Web-сайт источника кода
Url URL источника кода
Zone Зона Internet Explorer источника кода

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

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

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

Цели безопасного кодирования

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

Защита на основе признаков и защита по правам доступа кода предоставляют очень мощные, явные механизмы обеспечения безопасности. Коду большинства приложений достаточно задействовать инфраструктуру, предлагаемую .NET Framework. В некоторых случаях нужна дополнительная защита, специфичная для приложения и реализуемая либо расширением системы защиты, либо применением новых специализированных методов (ad hoc methods).

Подходы к безопасному кодированию

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

Код, нейтральный к защите

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

Библиотека, нейтральная к защите, обладает особыми характеристиками, которые вы должны понимать. Допустим, ваша библиотека предоставляет API-элементы, использующие файлы или вызывающие неуправляемый код; если у вашего кода нет соответствующего разрешения, он не будет работать, как задумано. Но, даже если у кода есть все разрешения, для его нормальной работы нужно, чтобы и у кода приложения, вызывающего ваш код, были те же разрешения. Если у вызывающего кода нет необходимых разрешений, при проверке стека защитой по правам доступа кода генерируется исключение, связанное с нарушением безопасности. Если от вызывающего кода можно требовать разрешений на все действия, выполняемые вашей библиотекой, то это легкий и надежный путь обеспечения безопасности, поскольку рискованного переопределения параметров защиты не происходит. Однако, если вы хотите оградить код приложения, вызывающего вашу библиотеку, от необходимости запрашивать разрешения (возможно, предоставляющих очень широкие права), изучите модель библиотеки, работающей с защищенными ресурсами (см. раздел "Библиотечный код, предоставляющей защищенные ресурсы" этого документа).

Код приложения, который не является повторно используемым компонентом

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

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

Дополнительную информацию см. в разделах этого документа Защита данных о состоянии и Пользовательский ввод.

Управляемая оболочка для машинного кода

Обычно в этом сценарии некая полезная функциональность реализована в виде машинного кода, и вы хотите сделать ее доступной управляемому коду, не переписывая машинный код. Управляемые оболочки (managed wrappers) легко пишутся с использованием механизма либо Platform Invoke (P/Invoke), либо COM Interop. Однако в таком случае вызывающие вашу оболочку программы должны иметь те же права, что и неуправляемый код. И если в системе действует политика по умолчанию, код, скачиваемый из интрасети или Интернета, работать с оболочками не будет.

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

Дополнительную информацию см. в разделах Неуправляемый код и Выдача разрешений.

Библиотечный код, предоставляющей защищенные ресурсы

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

Дополнительную информацию см. в разделах Неуправляемый код и Выдача разрешений.

Приемы безопасного кодирования

Примечание - Примеры кода написаны на C#, если не оговорено иное.

Запрос разрешений - отличный способ обеспечить поддержку защиты в разрабатываемом коде. Он позволяет:

 

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

Например:

 

[assembly:FileIOPermissionAttribute

 (SecurityAction.RequestMinimum, Write="C:\\test.tmp")]

[assembly:PermissionSet

 (SecurityAction.RequestOptional, Unrestricted=false)]

... SecurityAction.RequestRefused ...

В этом примере системе сообщается, что код не должен запускаться, пока не получит разрешение на запись в C:\test.tmp. Если одна из политик безопасности не предоставляет такое разрешение, генерируется исключение PolicyException и код не запускается. Вы должны убедиться, что вашему код выдается нужное разрешение, и тогда не придется беспокоиться об ошибках из-за нехватки разрешений.

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

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

Защита данных о состоянии

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

 

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

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

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

 

  • определять, какому коду разрешается наследовать от вашего класса. Можно указать, что это допускается только в той сборке, где находится класс, либо с помощью объявлений защиты требовать для наследования от вашего класса некоей идентификации или определенных разрешений (см. раздел Защита доступа к методам);
  • проверять, чтобы все производные классы реализовали аналогичную защиту или были "запечатаны" (sealed).

Упакованные типы значений

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

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

 

using System; 

using System.Reflection; 

using System.Reflection.Emit;

using System.Threading; 

using System.Collections;

class bug {

 // Допустим, у вас есть API-элемент, предоставляющий доступ к полю

 // через свойство, имеющее только аксессор get.

 public object m_Property;

 public Object Property {

   get { return m_Property;}

   set {m_Property = value;} // (если нужно)

 }

 // Значение этого свойства можно изменить, вызвав метод, который

 // передает параметр по ссылке и имеет следующую сигнатуру.

 public static void m1( ref int j ) {

   j = Int32.MaxValue;

 }

public static void m2( ref ArrayList j )

 {

  j = new ArrayList();

 }

 public static void Main(String[] args)

 {

  Console.WriteLine( "////// doing this with value type" );

  {

    bug b = new bug();

    b.m_Property = 4;

    Object[] objArr = new Object[]{b.Property};

    Console.WriteLine( b.m_Property );

    typeof(bug).GetMethod( "m1" ).Invoke( null, objArr );

    // Обратите внимание, что свойство изменилось.

    Console.WriteLine( b.m_Property );

    Console.WriteLine( objArr[0] );

  }

  Console.WriteLine( "////// doing this with a normal type" );

  {

    bug b = new bug();

    ArrayList al = new ArrayList();

    al.Add("elem");

    b.m_Property = al;

    Object[] objArr = new Object[]{b.Property};

    Console.WriteLine( ((ArrayList)(b.m_Property)).Count );

    typeof(bug).GetMethod( "m2" ).Invoke( null, objArr );

    // Обратите внимание, что свойство не изменилось.

    Console.WriteLine( ((ArrayList)(b.m_Property)).Count );

    Console.WriteLine( ((ArrayList)(objArr[0])).Count );

  }

 }

}

Защита доступа к методам

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

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

Вы можете ограничить доступ к методам в управляемом коде несколькими способами.

 

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

Аналогичным образом декларативная защита позволяет контролировать наследование классов. С помощью InheritanceDemand можно потребовать наличия определенной идентификации или разрешения от:

 

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

Пример: защита доступа к методу или классу

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

1. Команда sn -k создает пару из закрытого и открытого ключа. Закрытая часть нужна, чтобы подписать код строгим именем (strong name), и хранится в безопасном месте издателем кода. (Если она станет известной, указать вашу подпись в своем коде сможет кто угодно.)

Защита метода по строгому имени

 

sn -k keypair.dat

csc/r:App1.dll /a.keyfile:keypair.dat App1.cs

sn -p keypair.dat public.dat

sn -tp public.dat >publichex.txt



[StrongNameIdentityPermissionAttribute

 (SecurityAction.LinkDemand,

  PublicKey="...hex...",Name="App1",

  Version="0.0.0.0")]

public class Class1

2. Команда csc компилирует и подписывает App1, предоставляя ему доступ к защищенному методу.

3. Следующие две команды sn извлекают из пары открытый ключ и преобразуют его в шестнадцатеричную форму.

4. Во второй половине показанного исходного кода содержится фрагмент защищаемого метода. Пользовательский атрибут (custom attribute) определяет строгое имя и в шестнадцатеричном формате вставляет открытый ключ, полученный командой sn, в атрибут PublicKey.

5. В период выполнения App1 имеет необходимую подпись со строгим именем и может использовать Class1.

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

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

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

У сборок со строгими именами декларативная защита применяются ко всем открытым методам, свойствам и событиям, тем самым ограничивая доступ к ним лишь полностью доверяемыми методами, если только в сборке не указано противоположное с помощью атрибута AllowPartiallyTrustedCallers. Поэтому явно помечать классы, чтобы запретить их использование недоверяемым кодом, нужно только для неподписанных сборок или сборок с атрибутом AllowPartiallyTrustedCallers для подмножества типов, которое должно быть недоступным недоверяемому коду. Детали см. в документе Version 1 Security Changes for the Microsoft .NET Framework.

 

  • Для открытых незапечатанных (non-sealed) классов:

     

    [System.Security.Permissions.PermissionSetAttribute(System.Security.
    
      Permissions.SecurityAction.InheritanceDemand, Name="FullTrust")]
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.LinkDemand, Name="FullTrust")]
    
    public class CanDeriveFromMe
    
    

     

  • Для открытых запечатанных (sealed) классов:

     

    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.LinkDemand, Name="FullTrust")]
    
    public sealed class CannotDeriveFromMe
    
    

     

  • Для открытых абстрактных классов:

     

    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.InheritanceDemand, Name="FullTrust")]
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.LinkDemand, Name="FullTrust")]
    
    public abstract class CannotCreateInstanceOfMe_CanCastToMe
    
    

     

  • Для открытых виртуальных функций:

     

    class Base {
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.InheritanceDemand,
    
      Name="FullTrust")]
    
    [System.Security.Permissions.PermissionSetAttribute(
    
      System.Security.Permissions.SecurityAction.LinkDemand,
    
      Name="FullTrust")]
    
    public override void CanOverrideOrCallMe() { ... }
    
    

     

  • Для открытых абстрактных функций:

     

    class Base {
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.InheritanceDemand, Name="FullTrust")]
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.LinkDemand,
    
      Name="FullTrust")]
    
    public override void CanOverrideMe() { ... }
    
    

     

  • Для открытых переопределяющих функций, базовый класс которых не требует полного доверия:

     

    class Derived {
    
    [System.Security.Permissions.PermissionSetAttribute
    
    (System.Security.Permissions.SecurityAction.Demand, Name="FullTrust")]
    
    public override void CanOverrideOrCallMe() { ... }
    
    

     

  • Для открытых переопределяющих функций, базовый класс которых требует полного доверия:

     

    class Derived {
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.LinkDemand,
    
      Name="FullTrust")]
    
    public override void CanOverrideOrCallMe() { ... }
    
    

     

  • Для открытых интерфейсов:

     

    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.InheritanceDemand,
    
      Name="FullTrust")]
    
    [System.Security.Permissions.PermissionSetAttribute
    
      (System.Security.Permissions.SecurityAction.LinkDemand,
    
      Name="FullTrust")]
    
    public interface CanCastToMe
    
    

    Demand и LinkDemand

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

    Итак, декларативная защита поддерживает следующие проверки.

     

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

     

  • LinkDemand - происходит при компиляции по требованию (JIT) (в предыдущем примере такая проверка была бы проведена перед выполнением кода App1, обращающегося к Class1) и проверяет только непосредственно вызывающий код. При такой защите не проверяется тот, кто вызывает код, вызывающий ваш метод. Как только проверка заканчивается, дополнительных издержек этот вид защиты больше не создает - сколько бы раз не вызывался ваш метод. При использовании LinkDemand ваш интерфейс безопасен, но любой код, прошедший проверку и обращающийся к вашему коду, потенциально способен нарушить защиту, так как позволяет злонамеренному коду выполнять вызовы через авторизованный код. Поэтому не используйте LinkDemand, если полностью избавиться от потенциально слабых мест в защите нельзя.

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

     

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

    Интерфейсы и проверки LinkDemand

    Если виртуальный метод, свойство или событие с проверкой LinkDemand переопределяет метод базового класса, то к методу базового класса тоже должна применяться проверка LinkDemand, чтобы обеспечить безопасность переопределенного метода. Злонамеренный код может привести тип обратно к базовому и вызвать метод базового класса. Кроме того, имейте в виду, что проверки LinkDemand могут неявно добавляться к сборкам, у которых нет атрибута AllowPartiallyTrustedCallersAttribute уровня сборки.

    При защите реализаций методов с помощью LinkDemand желательно использовать LinkDemand и для методов интерфейса.

    Используя проверки LinkDemand при работе с интерфейсами, имейте в виду следующее.

     

    • Атрибут AllowPartiallyTrustedCallers может влиять на интерфейсы.
    • Вы можете указывать для интерфейсов проверки LinkDemand, чтобы выборочно запрещать частично доверяемому коду обращение к определенным интерфейсам, например при использовании атрибута AllowPartiallyTrustedCallers.
    • Если вы определили интерфейс в сборке, не содержащей атрибута AllowPartiallyTrustedCallers, то можете реализовать этот интерфейс для частично доверяемого класса.
    • Если указать LinkDemand для открытого метода класса, реализующего метод интерфейса, то LinkDemand не сработает при приведении типа к интерфейсу и вызове метода. В этом случае - из-за того, что вы осуществляете связывание с интерфейсом, - LinkDemand выполняется только для интерфейса.

    Кроме того, примите во внимание следующее.

     

    • Явные требования к связыванию (explicit link demands) для методов интерфейса. Убедитесь, что эти требования обеспечивают ожидаемый уровень защиты. Проверьте, может ли злонамеренный код использовать приведение типов, чтобы обойти требования к связыванию ранее описанным способом.
    • Виртуальные методы с требованиями к связыванию.
    • Типы и реализуемые ими интерфейсы должны согласованно использовать проверки LinkDemand.

    Внутренние виртуальные переопределения

    Учтите одну тонкость, если вы хотите, чтобы к вашему коду нельзя было обращаться из других сборок. Метод, объявленный как virtual и internal, может переопределять запись в таблице виртуальных методов (vtable) базового класса и доступен только в пределах своей сборки, так как является внутренним. Однако ключевое слово virtual делает метод доступным для переопределения, и он может быть переопределен из другой сборки, если ее код имеет доступ к самому классу. Чтобы исключить возможность переопределения, используйте декларативную защиту или удалите ключевое слово virtual, если в нем нет острой необходимости.

    Код оболочки

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

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

    Делегаты

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

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

    Проверки LinkDemand и оболочки

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

    Если полностью доверяемый код вызывает свойство, событие или метод, защищенный LinkDemand, вызов выполняется успешно при успешной проверке разрешений LinkDemand. Кроме того, если полностью доверяемый код предоставляет класс, который принимает имя свойства и вызывает его аксессор get через механизм отражения, то вызов аксессора get выполняется успешно, даже если у пользовательского кода нет прав доступа к этому свойству. Дело в том, что LinkDemand проверяет непосредственного вызывающего, который является полностью доверяемым кодом. По сути, полностью доверяемый код выполняет привилегированный вызов в интересах пользовательского кода, не проверяя, есть ли у него право на такой вызов. Если вы создаете оболочку для функциональности, использующей механизм отражения, прочтите статью Version 1 Security Changes for the Microsoft .NET Framework.

    Чтобы избежать дыр в защите, подобных описанной выше, исполняющая среда полностью проверяет стек при любом использовании метода Invoke (создавая экземпляр или вызывая метод, аксессор set или get) для вызова метода, конструктора, свойства или события, защищенного требованием к связыванию (link demand). Такая защита вызывает некоторое снижение производительности (одноуровневая проверка LinkDemand проходит быстрее) и изменяет семантику проверки защиты - полная проверка стека может потерпеть неудачу там, где одноуровневая проверка пройдет успешно.

    Оболочки, загружающие сборки

    Несколько методов, используемых для загрузки управляемого кода, в частности Assembly.Load(byte[]), загружают сборки с признаком (evidence) вызывающего. Если вы создаете оболочки для таких методов, система защиты при загрузке сборок будет использовать разрешения вашего кода вместо разрешений вызывающего. Вы вряд ли захотите, чтобы через вашу оболочку менее доверяемый код мог загружать по своему усмотрению код, которому выдаются более высокие разрешения по сравнению с вызывающим.

    Любой полностью доверяемый код или код, значительно более доверяемый по сравнению с потенциальными вызывающими (в том числе обладающими разрешениями уровня кода, загружаемого из Интернета), может оказаться уязвимым по отношению к такому ослаблению безопасности. Если в вашем коде есть открытый метод, который принимает массив байтов и передает его Assembly.Load(byte[]), тем самым создавая сборку в интересах вызывающего, это может привести к разрушению защиты.

    Такая проблема возникает при использовании следующих API-элементов:

     

    • System.AppDomain.DefineDynamicAssembly
    • System.Reflection.Assembly.LoadFrom
    • System.Reflection.Assembly.Load

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

    Выражение фильтра (filter expression), расположенное выше по стеку, вычисляется до выполнения любого оператора finally. Блок catch, связанный с этим фильтром, выполняется после оператора finally. Рассмотрим следующий псевдокод:

     

    void Main() {
    
        try {
    
            Sub();
    
        } except (Filter()) {
    
            Console.WriteLine("catch");
    
        }
    
    }
    
    bool Filter () {
    
        Console.WriteLine("filter");
    
        return true;
    
    }
    
    void Sub() {
    
        try {
    
            Console.WriteLine("throw");
    
            throw new Exception();
    
        } finally {
    
            Console.WriteLine("finally");
    
        }
    
    }                     
    
    

    Вот что выводит этот код:

     

    Throw
    
    Filter
    
    Finally
    
    Catch
    
    

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

     

                try {
    
                            Alter_Security_State();
    
                               // Здесь возможны любые изменения (изменение
    
                               // переменных состояния, переключение в
    
                               // неуправляемый контекст, олицетворение и т. д.).
    
                               // Этим может воспользоваться злонамеренный код,
    
                               // выполняемый до восстановления состояния.
    
                            Do_some_work();
    
                } finally {
    
                            Restore_Security_State();
    
                               // Здесь просто восстанавливается ранее
    
                               // измененное состояние.
    
                }
    

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

    Рекомендуемое решение - ввести обработчик исключения, изолирующий изменения в состоянии потока вашего кода от блоков фильтрации вызывающих. Однако важно правильно установить обработчик исключения, иначе решить эту проблему не удастся. В следующем примере код на Microsoft Visual Basic® переключает культуру пользовательского интерфейса (UI), но аналогичным образом возможны и другие изменения состояния потока.

     

    YourObject.YourMethod()
    
    {
    
       CultureInfo saveCulture = Thread.CurrentThread.CurrentUICulture;
    
       try {
    
          Thread.CurrentThread.CurrentUICulture = new CultureInfo("de-DE");
    
          // Выполняется действие, приводящее к генерации исключения.
    
    }
    
       finally {
    
          Thread.CurrentThread.CurrentUICulture = saveCulture;
    
       }
    
    }
    
    
    
    Public Class UserCode
    
       Public Shared Sub Main()
    
          Try
    
             Dim obj As YourObject = new YourObject
    
             obj.YourMethod()
    
          Catch e As Exception When FilterFunc
    
             Console.WriteLine("An error occurred: '{0}'", e)
    
             Console.WriteLine("Current Culture: {0}",
    
    Thread.CurrentThread.CurrentUICulture)
    
          End Try
    
       End Sub
    
    
    
       Public Function FilterFunc As Boolean
    
          Console.WriteLine("Current Culture: {0}", Thread.CurrentThread.CurrentUICulture)
    
          Return True
    
       End Sub
    
    
    
    End Class
    
    

    Правильным решением в этом случае является включение существующего блока try/finally в блок try/catch. Простое указание catch-throw в существующем блоке try/finally не решает проблему:

     

    YourObject.YourMethod()
    
    {
    
       CultureInfo saveCulture = Thread.CurrentThread.CurrentUICulture;
    
    
    
       try {
    
          Thread.CurrentThread.CurrentUICulture = new CultureInfo("de-DE");
    
          // Выполняется действие, приводящее к генерации исключения.
    
    }
    
    catch { throw; }
    
       finally {
    
          Thread.CurrentThread.CurrentUICulture = saveCulture;
    
       }
    
    }
    
    

    Проблема не решается, так как оператор finally не выполняется до тех пор, пока функция FilterFunc не получит управление.

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

     

    YourObject.YourMethod()
    
    {
    
       CultureInfo saveCulture = Thread.CurrentThread.CurrentUICulture;
    
       try {
    
          try {
    
             Thread.CurrentThread.CurrentUICulture = new CultureInfo("de-DE");
    
             // Выполняется действие, приводящее к генерации исключения.
    
    }
    
          finally {
    
             Thread.CurrentThread.CurrentUICulture = saveCulture;
    
          }
    
       }
    
       catch { throw; }
    
    }
    
    

    Неуправляемый код

    Иногда библиотечный код должен вызывать неуправляемый код (например, встроенные функции такого API, как Win32). Поскольку это означает выход за периметр защиты управляемого кода, соблюдайте осторожность. Если ваш код нейтрален к защите (см. раздел Код, нейтральный к защите), то и ваш код, и код, который его вызывает, должен иметь разрешения на выполнение неуправляемого кода (SecurityPermission.UnmanagedCode).

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

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

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

     

  • Функциональность. Обеспечивает ли неуправляемый API безопасную функциональность, не позволяющую путем вызова этого кода выполнять потенциально опасные операции? При защите по правам доступа кода для предоставления доступа к ресурсам используются разрешения, поэтому нужно выяснить, использует ли код API файлы, UI-элементы, многопоточность (threading) и не открывает ли доступ к защищенной информации. Если да, то, прежде чем разрешать вызов кода API, управляемая оболочка должна требовать наличия необходимых разрешений. Кроме того, пока нет защиты с помощью разрешений, для обеспечения безопасности необходимо ограничение доступа к памяти с помощью строгого контроля типов (strict type safety).

     

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

    Использование SuppressUnmanagedCodeSecurity

    При объявлении прав (asserting) и последующем вызове неуправляемого кода следует принимать во внимание вопросы производительности. При каждом таком вызове система защиты автоматически запрашивает разрешение неуправляемого кода, из-за чего каждый раз проверяется весь стек. Если вы объявили права и тут же вызвали неуправляемый код, проверять стек бессмысленно: в нем содержатся только ваши объявление прав и вызов неуправляемого кода.

    Чтобы отключить обычную проверку защиты, требующую SecurityPermission.UnmanagedCode, для точек входа в неуправляемый код можно указывать пользовательский атрибут SuppressUnmanagedCodeSecurity. При этом будьте крайне осторожны, так как здесь открывается возможность вызова неуправляемого кода без проверки защиты. Следует отметить, что даже при использовании SuppressUnmanagedCodeSecurity выполняется разовая проверка защиты (в момент компиляции по требованию), чтобы убедиться, есть ли у непосредственно вызывающего разрешение на вызов неуправляемого кода.

    При использовании атрибута SuppressUnmanagedCodeSecurity учитывайте следующее.

     

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

    Схема именования методов неуправляемого кода

    Для методов неуправляемого кода весьма полезна и настоятельно рекомендуется следующая схема именования. Все методы неуправляемого кода делятся на три категории: safe (безопасный), native ("родной") и unsafe (небезопасный). Эти ключевые слова можно использовать как имена классов, содержащих точки входа в неуправляемый код. В исходном коде эти ключевые слова следует добавлять к имени класса, например Safe.GetTimeOfDay, Native.Xyz или Unsafe.DangerousAPI. Принадлежность к любой из этих категорий однозначно указывает разработчикам, как использовать метод (см. таблицу ниже).

     

    Ключевое слово Соглашения по безопасности
    safe Полная безвредность при вызове любым кодом (даже злонамеренным). Может использоваться точно так же, как и управляемый код. Пример: получение времени дня.
    native Нейтральный к защите, т. е. неуправляемый код, требующий для вызова разрешения неуправляемого кода. Безопасность проверяется, что предотвращает вызов неавторизованным кодом.
    unsafe Потенциально опасная точка входа в неуправляемый код с отключенной защитой. Разработчикам следует использовать такой код с величайшей осторожностью и только при том условии, что применяются другие способы защиты, не допускающие нарушения безопасности. При использовании таких методов разработчики сами отвечают за создание системы защиты.

    Пользовательский ввод

    Пользовательские данные, каким бы образом они ни передавались (через Web-запрос или URL, ввод в элементы управления Microsoft Windows Forms и т. д.), могут неблагоприятно повлиять на код, так как эти данные часто напрямую используются в качестве параметров при вызове другого кода. Эта ситуация аналогична той, где злонамеренный код вызывает ваш код, передавая недопустимые параметры, так что она требует принятия тех же мер предосторожности. На практике сделать пользовательский ввод безопасным гораздо труднее, так как в этом случае нет стека, где можно было бы проверить наличие потенциально недоверяемых данных.

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

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

    · Любые пользовательские данные, возвращаемые сервером, обрабатываются на клиенте в контексте сайта сервера. Если ваш Web-сервер получает данные от пользователя и вставляет их в возвращаемую Web-страницу, то они могут, например, содержать тэг <script> и выполняться так, будто получены с сервера.

     

    • Помните, что клиент может запросить любой URL.
    • Учитывайте возможность ввода "хитрых" или неправильных путей:
      • ..\ , а также крайне длинных путей;
      • использования знаков подстановки (*);
      • раскрытия маркеров (%token%);
      • путей странного вида, имеющих особый смысл;
      • альтернативных имен NTFS-потоков (streams), например имя_файла::$DATA;
      • кратких версий имен файлов, например longfilename и longfi~1.
    • Eval(пользовательские_данные) позволяет делать что угодно.
    • Позднее связывание (late binding) с именем, включающим некие пользовательские данные.
    • Если вы имеете дело с Web-данными, учитывайте допустимость разных видов escape-последовательностей, в том числе:
      • шестнадцатеричных последовательностей (%nn);
      • Unicode-последовательностей (%nnn);
      • очень длинных последовательностей UTF-8 (%nn%nn);
      • двойных последовательностей (%nn превращается в %mmnn, где %mm - escape-последовательность для '%').
    • Будьте осторожны с именами пользователей, которые могут быть представлены более чем в одном каноническом формате. Так, в Microsoft Windows 2000 вместо username@redmond.microsoft.com нередко используется REDMOND\username.

    Удаленное взаимодействие

    Удаленное взаимодействие (remoting) позволяет реализовать прозрачные вызовы между доменами приложений (application domains, AppDomains), процессами или компьютерами. Однако защита по правам доступа кода не позволяет проверять стек при межмашинном или межпроцессном вызове (такая проверка выполняется только при вызовах между доменами приложений в одном и том же процессе).

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

    Кроме того, обычно не следует предоставлять доступ к методам, свойствам или событиям, защищенным декларативными проверками защиты LinkDemand и InheritanceDemands. При удаленном взаимодействии эти проверки не выполняются. Другие проверки, например Demand, Assert и т. д., выполняются при взаимодействии между доменами приложения в процессе, но не между процессами и компьютерами.

    Защищенные объекты

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

    Рассмотрим, например, создание объекта FileStream. Во время создания запрашивается разрешение FileIOPermission и, если оно получено, возвращается файловый объект. Однако, если ссылка на этот объект передается объекту, не имеющему файловых разрешений, то последний получает возможность выполнять чтение/запись в данный файл.

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

    Сериализация

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

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

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

    Интерфейс ISerializable предназначен для использования только инфраструктурой сериализации. Однако, если он не защищен, через него можно получить доступ к закрытой информации. Если вам нужна нестандартная сериализация с реализацией интерфейса ISerializable, примите следующие меры предосторожности.

     

    • GetObjectData следует явным образом защищать, требуя разрешение SecurityPermission.SerializationFormatter или добиваясь, чтобы в выходных данных метода не было закрытой информации. Например:

       
      [SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter
      =true)]
      public override void GetObjectData(SerializationInfo info,
      StreamingContext context)
      
      
    • Специальный конструктор, применяемый при сериализации, должен тщательно проверять входные данные и быть защищенным или закрытым, чтобы не допускать использования злонамеренным кодом. Этот конструктор должен выполнять те же проверки защиты и требовать те же разрешения, что и при создании экземпляра такого класса любыми другими средствами (путем явного или неявного создания с помощью некоей фабрики объектов).

    Проблемы, связанные с пересечением границ доменов приложений

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

    Один домен приложения может заставить другой загрузить сборку и выполнить содержащийся в ней код вызовом прокси объекта, находящегося в другом домене приложения. Чтобы получить прокси из другого домена приложения, домен приложения, содержащий объект, должен передать его (как параметр метода или возвращаемое значение). Кроме того, если домен приложения только что создан, его создатель получает прокси объекта AppDomain. Таким образом, чтобы избежать нарушения изоляции кода, более доверяемый домен приложения не должен передавать ссылки на свои объекты MarshalByRefObject менее доверяемым доменам приложений.

    Обычно домен приложения по умолчанию (default application domain) создает дочерние домены приложения с управляющим объектом (control object) в каждом из них. Этот объект управляет новым доменом приложения и иногда получает указания от домена приложения по умолчанию, но не может взаимодействовать с доменом напрямую. В некоторых случаях домен приложения по умолчанию вызывает свой прокси управляющего объекта. Однако возможны случаи, когда управляющему объекту необходимо выполнять обратный вызов домена приложения по умолчанию. Тогда домен приложения по умолчанию передает объект обратного вызова с маршалингом по ссылке конструктору управляющего объекта. За защиту этого прокси отвечает управляющий объект. Если управляющий объект поместит прокси в открытое статическое поле или открытый класс или каким-то другим способом откроет доступ к прокси, другой код сможет выполнять обратный вызов домена приложения по умолчанию, что весьма опасно. Поэтому управляющие объекты всегда неявно хранят прокси в закрытом виде.

    Выдача разрешений

    Защита на основе признаков основана на предположении, что высокий уровень доверия (с широкими полномочиями) присваивается лишь коду, заслуживающему доверия, а злонамеренный код является мало доверяемым или вообще не имеет разрешений. В политике по умолчанию в .NET Framework разрешения выдаются на основе зон (так, как они определены в Microsoft Internet Explorer). Ниже приведено упрощенное описание этой политики по умолчанию.

     

    • Зона локального компьютера (например, c:\app.exe) является полностью доверяемой. Предполагается, что пользователи помещают на свой компьютер только код, которому они доверяют, и что большинство пользователей не собираются разбивать свой жесткий диск на области с разной степенью доверия. По существу, этот код может делать все, что угодно, поэтому от злонамеренного кода, находящегося в этой зоне, никакой защиты нет.
    • Зона Интернета (например, http://www.microsoft.com/). Коду из этой зоны предоставляется очень ограниченный набор разрешений, который неопасно предоставить даже злонамеренному коду. Обычно этому коду нельзя доверять, поэтому его можно безопасно выполнять только с очень узкими разрешениями, с которыми он не сумеет нанести ущерб:
      • WebPermission - доступ к серверу сайта, с которого получен код;
      • FileDialogPermission - доступ только к файлам, специально указанным пользователем;
      • IsolatedStorageFilePermission - постоянное хранилище, ограниченное пределами Web-сайта;
      • UIPermission - возможность записи информации в окно пользовательского интерфейса.
    • Зона интрасети (например, \\UNC\share). Код из этой зоны выполняется с чуть большими разрешениями, чем код из Интернета, но среди них все равно нет таких, которые предоставляли бы широкие полномочия:
      • FileIOPermission - доступ только для чтения к файлам каталога, из которого загружен код;
      • WebPermission - доступ к серверу, с которого загружен код;
      • DNSPermission - допускается разрешение DNS-имен в IP-адреса;
      • FileDialogPermission - доступ только к файлам, специально указанным пользователем;
      • IsolatedStorageFilePermission - постоянное хранилище (с меньшими ограничениями);
      • UIPermission - код может свободно использовать собственные окна верхнего уровня.
    • Зона ограниченных сайтов, код из которой выполняется только с минимальными разрешениями.

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

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

    Опасные разрешения

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

    К опасным разрешениям относятся:

     

    • SecurityPermission:
       
      • UnmanagedCode - позволяет управляемому коду вызывать неуправляемый код, что зачастую весьма опасно;
      • SkipVerification - код может делать что угодно без всякой верификации;
      • ControlEvidence - управление признаками позволяет обмануть систему защиты;
      • ControlPolicy - возможность изменять политику безопасности позволяет отключить защиту;
      • SerializationFormatter - за счет сериализации можно обойти управление доступом (см. раздел по сериализации);
      • ControlPrincipal - возможность указывать текущего пользователя позволяет обходить защиту на основе ролей;
      • ControlThread - возможность манипуляций с потоками опасна, так как с ними связано состояние защиты;
    • ReflectionPermission:
      • MemberAccess - позволяет отключать механизм управления доступом (становится возможным использование закрытых членов).

    Защита и конкуренция

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

    Конкуренция в методе Dispose

    Если метод Dispose класса не синхронизирован, код очистки в этом методе может быть выполнен более одного раза. Рассмотрим такой код:

     

    void Dispose() {
    
       if( _myObj != null ) {
    
          Cleanup(_myObj);
    
          _myObj = null;
    
       }
    
    }
    
    

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

    Конкуренция в конструкторах

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

    Конкуренция при использовании кэшированных объектов

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

     

    void SomeSecureFunction() {
    
       if(SomeDemandPasses()) {
    
          _fCallersOk = true;
    
          DoOtherWork();
    
          _fCallersOk = false;
    
       }
    
    }
    
    void DoOtherWork() {
    
       if(  _fCallersOK ) {
    
          DoSomethingTrusted();
    
       }
    
       else {
    
          DemandSomething();
    
          DoSomethingTrusted();
    
       }
    
    }
    
    

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

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

    Конкуренция при подготовке объекта к уничтожению

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

    Другие технологии защиты

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

    Генерация кода "на лету"

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

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

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

    Продумайте, не может ли злонамеренная программа изменить код при его компиляции. Посмотрите, не возникает ли небольшой промежуток времени, в течение которого злонамеренный код способен изменить исходный код на диске перед чтением компилятором или загрузкой DLL вашим кодом? Если да, защищайте каталог с этими файлами, используя в зависимости от ситуации защиту по правам доступа кода или список управления доступом (Access Control List, ACL) файловой системы.

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

    Выполняйте сгенерированный код с наименьшими возможными разрешениями (используя PermitOnly или Deny).

    Защита на основе ролей: аутентификация и авторизация

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

    Управление секретами

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

    Шифрование и подписи

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

    Случайные числа

    Если для системы защиты нужно генерировать случайные числа и требуется, чтобы они были истинно случайными, используйте System.Security.Cryptography.RandomNumberGenerator. Применение генераторов псевдослучайных чисел приводит к их предсказуемости, чем может воспользоваться злоумышленник.

    Проблемы установки

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

     

    1. Настройте систему на использование двух разделов жесткого диска.
    2. Переформатируйте второй раздел; не изменяйте ACL по умолчанию в корневом каталоге диска.
    3. Установите свою программу, сменив каталог установки на каталог, создаваемый во втором разделе.

    Проверьте следующее.

     

    1. Имеется ли код, выполняемый как служба или обычно запускаемый пользователями с правами администратора и доступный для записи кому угодно?
    2. Если код устанавливался на терминальный сервер, работающий в режиме сервера приложений: могут ли одни пользователи записывать двоичные файлы, а другие - их запускать?
    3. Есть ли файлы в системном каталоге или его подкаталогах, доступные для записи пользователям, отличным от администраторов?

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


  • Может пригодится:


    Автор: Корпорация Microsoft
    Прочитано: 6102
    Рейтинг:
    Оценить: 1 2 3 4 5

    Комментарии: (12)

    Прислал: новости
    спасибо. больше всего понравилось "Генерация кода "на лету"

    Прислал: soft
    и пишет человек без ошибок, что в наше время редкость

    Прислал: боря
    привет

    Прислал: вася
    а сам то часто

    Прислал: вова
    что часто то

    Прислал: федя
    без ошибок

    Прислал: Nikolay
    Отличный сайт. Мне друг порекомендовал... респект вам! Привет другу! он меня поймет ;)

    Прислал: mp3-mp3-mp3-mp3
    ghbdtn

    Прислал: purelostfilms
    purelostfilms

    Прислал: rssautonews
    rssautonews

    Прислал: vasja
    привет

    Прислал: федя
    привет

    Добавить комментарий
    Ваше имя*:
    Ваш email:
    URL Вашего сайта:
    Ваш комментарий*:
    Код безопастности*:

    Рассылка новостей
    Рейтинги
    © 2007, Программирование Исходники.Ру