式インジェクション

C#のSystem.Linq.Dynamic.Coreにおける式インジェクション

説明

System.Linq.Dynamic.Coreは文字列を解析してLINQ式ツリーを作成します。リクエストの文字列全体をWhere、OrderBy、Select、DynamicExpressionParser.ParseLambdaの式の引数に渡すと、呼び出し元が条件、並べ替え、射影などのクエリ構造を変更できます。

固定した式の@0、@1や、ライブラリーのWhereInterpolatedで値を渡せば、値と式の構造を分離できます。文字列の連結や通常の文字列補間で入力を式に埋め込むこと、完成した式そのものを受け取ることは、パラメーター化ではありません。

この文書のバージョン別の参照はSystem.Linq.Dynamic.Core 1.7.3を対象とします。1.3.0と1.6.0では、呼び出せるメソッドと型、objectのメソッド、OrderByの式にセキュリティ上の制限が追加されました。従来のコード実行やリフレクションへのアクセスの危険を減らしますが、利用者にアプリケーションの絞り込みや射影の方針を選ばせてもよいという意味ではありません。

想定される影響

  • 意図したフィルターやテナントの境界を回避し、権限のないレコードを取得される可能性があります。
  • 許可していない射影や並べ替えによって、機密データが漏れる可能性があります。
  • 複雑で負荷の高い式により、データベースやアプリケーションの資源を消費される可能性があります。
  • 1.3.0未満の脆弱なバージョンでは、CVE-2023-32571による任意のコードやコマンドの実行につながる可能性があります。1.6.0未満には、CVE-2024-51417に関連するリフレクションの型や静的メンバーへのアクセスの危険があります。

対処方法

  1. 文字列を使うDynamic LINQより、強く型付けされたLINQを優先してください。
  2. 短い識別子を、サーバーが管理するExpression<Func<T, bool>>や固定の式文字列に対応付けてください。入力全体を式として扱わないでください。
  3. 値だけを変える場合は式を固定し、@0、@1、WhereInterpolatedを使ってください。プロパティ名、演算子、並べ替え方向はパラメーター化されないため、別の固定マップで選択します。
  4. System.Linq.Dynamic.Coreを保守されているリリースに更新してください。AllowNewToEvaluateAnyType = false、AllowEqualsAndToStringMethodsOnObject = false、RestrictOrderByToPropertyOrField = trueなど、制限の厳しいParsingConfigの既定値を維持してください。独自の型プロバイダーでは必要な型だけを公開します。
  5. データへのアクセス権限を別途適用し、結果数、クエリ時間、複雑さを制限してください。

例

変更前

csharp
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Linq.Dynamic.Core;

public sealed class User
{
    public bool IsActive { get; init; }
    public int Age { get; init; }
}

public sealed class SearchController
{
    public IQueryable<User> Search(
        [FromQuery] string predicate,
        IQueryable<User> users)
    {
        return users.Where(predicate);
    }
}

変更後

csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

public sealed class User
{
    public bool IsActive { get; init; }
    public int Age { get; init; }
}

public sealed class SearchService
{
    private static readonly IReadOnlyDictionary<string, Expression<Func<User, bool>>> Filters =
        new Dictionary<string, Expression<Func<User, bool>>>(StringComparer.Ordinal)
        {
            ["active"] = user => user.IsActive,
            ["adult"] = user => user.Age >= 18
        };

    public IQueryable<User> Search(string filter, IQueryable<User> users)
    {
        if (!Filters.TryGetValue(filter, out var predicate))
        {
            throw new ArgumentException("Unsupported filter.", nameof(filter));
        }

        return users.Where(predicate);
    }
}

変更前はpredicateをDynamic LINQの構文として直接解析します。変更後は入力を固定のキーとしてだけ使い、サーバーが作成した型付きの条件を選択します。

Dynamic LINQが必要で、外部から受け取るのが値だけなら、users.Where("Age >= @0", minimumAge)のように構造を固定してください。minimumAgeが文字列でも、式として再解析されず、@0の値としてバインドされます。

参考資料