OData セキュリティ ガイダンス (翻訳)

ASP.NET 公式サイトの OData Security Guidance (by Mike Wasson) を翻訳したものです。


この記事では、OData を通じてデータセットを公開する際に検討すべき、いくつかのセキュリティの議論について記述します。

EDM のセキュリティ

クエリのセマンティクスは、元のモデル型ではなく、Entity Data Model (EDM) に基づきます。
EDM からはプロパティを除外でき、その場合はクエリには公開されません。
例えば、モデルの中に Salary プロパティを持つ Employee 型があるとします。
クライアントから隠すため、EDM からこのプロパティを除外したいでしょう。

EDM からプロパティを除外する方法は 2 つあります。
モデル クラスのプロパティには、[IgnoreDataMember] 属性を設定できます。

public class Employee
{
    public string Name { get; set; }
    public string Title { get; set; }
    [IgnoreDataMember]
    public decimal Salary { get; set; } // EDM では非表示
}

また、プログラム上で EDM からプロパティを除外できます。

var employees = modelBuilder.EntitySet<Employee>("Employees");
employees.EntityType.Ignore(emp => emp.Salary);

クエリのセキュリティ

意地悪な利用者や経験不足な利用者は、実行に時間のかかるクエリを作るかもしれません。
最悪の場合、サービスへのアクセスが中断されることがあります。

[Queryable] 属性は、クエリを解析、検証、そして適用するためのアクション フィルターです。
このフィルターはクエリ オプションを LINQ 式に変換します。
OData コントローラーが IQueryable 型を返す場合、IQueryable LINQ プロバイダーは LINQ 式をクエリに変換します。
したがって、パフォーマンスは LINQ プロバイダーに依存し、またデータセットやデータベースのスキーマの特性にも依存します。

ASP.NET Web API における OData クエリ オプションの利用についての詳細は
Supporting OData Query Options を参照してください。

もしすべての利用者を信頼できる (例えば、社内環境) か、またはデータセットが小さい場合、
クエリのパフォーマンスは問題にならないでしょう。
そうでないのであれば、以下に紹介する内容を検討すべきです。

  • さまざまなクエリでサービスをテストし、データベースをプロファイルします。
  • サーバー駆動ページングを有効にして、1 回のクエリで大きなデータセットを返すことを避けます。
    詳細は Server-Driven Paging を参照してください。

// サーバー駆動ページングを有効にします。
[Queryable(PageSize = 10)]

  • $filter や $orderby は必要でしょうか?
    $top や $skip を使用するクライアント ページングは許可しても、
    他のクエリ オプションを無効にするようなアプリケーションもあるでしょう。

// クライアント ページングを許可しますが、他のクエリ オプションを除外します。
[Queryable(AllowedQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]

  • $orderby を、クラスター化インデックスの設定されたプロパティに限定することを検討します。
    大きなデータをクラスター化インデックスなしでソートするには時間がかかります。

// $orderby で許可されるプロパティを設定します。
[Queryable(AllowedOrderByProperties = "Id,Name")] // コンマ区切りのリスト

  • 最大ノード数: [Queryable] の MaxNodeCount プロパティでは、$filter 構文木で許可される最大ノード数を設定します。
    既定値は 100 ですが、ノード数が大きいとコンパイルが遅くなるため、これよりも小さな値を設定するほうがよいでしょう。
    このことは、LINQ to Objects (メモリ内のコレクションにおける LINQ クエリで、中間的な LINQ プロバイダーを利用しない)
    を利用する場合にとくに当てはまります。

// 最大ノード数を設定します。
[Queryable(MaxNodeCount = 20)]

  • any() および all() 関数を無効にすることを検討します。遅くなる可能性があるためです。

// any() および all() 関数を無効にします。
[Queryable(AllowedFunctions = AllowedFunctions.AllFunctions &
    ~AllowedFunctions.All & ~AllowedFunctions.Any)]

  • プロパティが大きな文字列を含んでいる場合 (例えば、商品の説明やブログのエントリ)、
    文字列関数を無効にすることを検討します。

// 文字列関数を無効にします。
[Queryable(AllowedFunctions = AllowedFunctions.AllFunctions &
    ~AllowedFunctions.AllStringFunctions)]

  • ナビゲーション プロパティによるフィルターを無効にすることを検討します。
    ナビゲーション プロパティによるフィルターは、データベースのスキーマによっては時間のかかる結合処理を招くでしょう。
    次のコードは、ナビゲーション プロパティによるフィルターを防ぐためのクエリ バリデーターを示します。
    クエリ バリデーターについての詳細は Query Validation を参照してください。

// ナビゲーション プロパティによるフィルターを防ぐためのバリデーターです。
public class MyFilterQueryValidator : FilterQueryValidator
{
    public override void ValidateNavigationPropertyNode(
        Microsoft.Data.OData.Query.SemanticAst.QueryNode sourceNode,
        Microsoft.Data.Edm.IEdmNavigationProperty navigationProperty,
        ODataValidationSettings settings)
    {
        throw new ODataException("No navigation properties");
    }
}

  • データベースに対して特化されたバリデーターを記述することにより、$filter クエリを制限することを検討します。
    例えば、これらの 2 つのクエリを考えます:

    ・ 姓が ‘A’ から始まる俳優が出演するすべての映画。
    ・ 1994 年に公開された映画。

    映画が俳優でインデックスされていない限り、
    1 つ目のクエリはデータベース エンジンにすべての映画のリストをスキャンさせるでしょう。
    一方、2 つ目のクエリは、映画が公開年でインデックスされていると仮定すれば許容できるでしょう。

    次のコードは、"ReleaseYear" および "Title" プロパティによるフィルターのみを許容するバリデーターを示します。

// $filter 式で利用可能なプロパティを制限するバリデーターです。
public class MyFilterQueryValidator : FilterQueryValidator
{
    static readonly string[] allowedProperties = { "ReleaseYear", "Title" };

    public override void ValidateSingleValuePropertyAccessNode(
        SingleValuePropertyAccessNode propertyAccessNode,
        ODataValidationSettings settings)
    {
        string propertyName = null;
        if (propertyAccessNode != null)
        {
            propertyName = propertyAccessNode.Property.Name;
        }

        if (propertyName != null && !allowedProperties.Contains(propertyName))
        {
            throw new ODataException(string.Format("Filter on {0} not allowed", propertyName));
        }
        base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, settings);
    }
}

  • 一般的に、どの $filter 関数が必要かを検討します。
    もし利用者が $filter のすべての表現を必要としないのであれば、許可する関数を制限できます。

参照
OData : The Official Microsoft ASP.NET Site
ASP.NET and Web Tools 2012.2
ASP.NET Web APIのODataでQueryable APIにバリデーションを適用する

カテゴリー: .NET Framework. タグ: , . Leave a Comment »

現在のアプリケーションを管理者権限で再実行させる

.NET アプリケーションの実行時に、管理者権限で実行されていない場合は動的に昇格させて再実行させる方法について記述します。

ポイントは次の通りです。

  • WindowsPrincipal オブジェクトを利用して、現在の実行ユーザーが管理者グループに含まれているかどうかを判定する。
  • 現在のアプリケーションを新しいプロセスで実行する。
    動詞に runas を指定することで、UAC の権限昇格ダイアログが現れる。
  • UAC の権限昇格ダイアログで [いいえ] が選択された場合、呼び出し元に Win32Exception がスローされる。

以下がコードのサンプルです。コンソール アプリケーションとして作成しています。


using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;

namespace ConsoleApplication1
{
    static class Program
    {
        static void Main(string[] args)
        {
            if (IsAdmin)
            {
                // 本来の処理を実行します。
                MainAsAdmin(args);
            }
            else
            {
                // 自身を runas で実行します。
                RunSelfAsAdmin(args);
            }
        }

        static void MainAsAdmin(string[] args)
        {
            // 例として、Program Files 内にファイルを作成します。
            var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Temp", "RunAsTest.txt");
            Directory.CreateDirectory(Path.GetDirectoryName(filePath));
            File.WriteAllText(filePath, "Test Text", Encoding.UTF8);
        }

        static bool IsAdmin
        {
            get
            {
                var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
                return principal.IsInRole(WindowsBuiltInRole.Administrator);
            }
        }

        static void RunSelfAsAdmin(string[] args)
        {
            var assembly = Assembly.GetEntryAssembly();
            var startInfo = new ProcessStartInfo(assembly.Location, ToCommandArgs(args))
            {
                UseShellExecute = true,
                Verb = "runas",
            };

            try
            {
                Process.Start(startInfo);
            }
            catch (Win32Exception ex)
            {
                // ユーザーが [いいえ] を選択すると例外が発生します。
                Console.WriteLine(ex.Message);
            }
        }

        static readonly Func<string, string> EscapeCommandArg = x => x.Contains(‘ ‘) ? string.Format("\"{0}\"", x) : x;
        static readonly Func<string[], string> ToCommandArgs = x => string.Join(" ", x.Select(EscapeCommandArg));
    }
}


このアプリケーションを通常の方法で (ダブルクリックなどで) 実行すると、UAC の権限昇格ダイアログが表示されます。
この時点では、「Process.Start(startInfo);」の部分で実行がブロックされています。

ユーザー アカウント制御

[はい] を選択すればアプリケーションが新しいプロセスで再実行され、IsAdmin プロパティの値が true になります。

なお、上記の場合は IsAdmin プロパティを一度だけしか呼び出していませんが、
これをアプリケーション内で頻繁に呼び出す場合は、
次のように最初に一度だけ WindowsPrincipal オブジェクトを作成して、スレッドに設定しておく方法が考えられます。


static bool isPrincipalInitialized;
static bool IsAdmin
{
    get
    {
        if (!isPrincipalInitialized)
        {
            AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
            isPrincipalInitialized = true;
        }
        var principal = (WindowsPrincipal)Thread.CurrentPrincipal;
        return principal.IsInRole(WindowsBuiltInRole.Administrator);
    }
}


バージョン情報
.NET Framework 4

参照
UACが有効の時、アプリケーションを管理者に昇格させて起動する (DOBON.NET)
管理者としてほかのアプリケーションを実行するには? (@IT)

カテゴリー: .NET Framework, OS. タグ: . Leave a Comment »

メソッドの引数の名前を式ツリーから取得する

メソッドに渡された引数の値が null でないことや空文字でないことなどを検証するために、
次のようなコードを記述することと思います。


static void Method1(string parameter1)
{
    if (parameter1 == null) throw new ArgumentNullException("parameter1");
    if (parameter1.Length == 0) throw new ArgumentException("値を空にすることはできません。", "parameter1");

    // 以下省略。
}


このようなコードは頻繁に必要になるので、コード スニペットを用意しておくと便利です

メソッド チェーンにしたい場合は、次のように拡張メソッドを使います。


static class Program
{
    static string[] Separate(this string text)
    {
        return text
            .AssertArgumentNotNull("text")
            .AssertArgumentNotEmpty("text")
            .Split(‘:’)
            .Select(s => s.Trim())
            .ToArray();
    }
}

[DebuggerNonUserCode]
public static class Assert
{
    public static T AssertArgumentNotNull<T>(this T value, string name) where T : class
    {
        if (value == null) throw new ArgumentNullException(name); 
        return value;
    }

    public static string AssertArgumentNotEmpty(this string value, string name)
    {
        if (value.Length == 0) throw new ArgumentException("値を空にすることはできません。", name); 
        return value;
    }
}


上記の方法ではメソッドの引数の名前を文字列のリテラル ("parameter1" のような形式) で記述することになりますが、
どうしてもこれを避けたい場合、ラムダ式による式ツリー (式木, Expression Tree) を利用することによって実現できます。


static class Program
{
    static void Method1(string parameter1)
    {
        Assert.IsArgumentNotNull(() => parameter1);
        Assert.IsArgumentNotEmpty(() => parameter1);

        // 以下省略。
    }
}

[DebuggerNonUserCode]
public static class Assert
{
    public static void IsArgumentNotNull<T>(Expression<Func<T>> getValue) where T : class
    {
        var body = (MemberExpression)getValue.Body;
        var field = (FieldInfo)body.Member;
        var value = getValue.Compile()();

        if (value == null) throw new ArgumentNullException(field.Name);
    }

    public static void IsArgumentNotEmpty(Expression<Func<string>> getValue)
    {
        var body = (MemberExpression)getValue.Body;
        var field = (FieldInfo)body.Member;
        var value = getValue.Compile()();

        if (value.Length == 0) throw new ArgumentException("値を空にすることはできません。", field.Name);
    }
}


Expression<TDelegate>.Compile メソッドを呼び出すことで、式ツリーをデリゲートに変換しています。

バージョン情報
.NET Framework 3.5, 4

参照
式ツリー (C# および Visual Basic)