Quantcast
Channel: @jsakamoto
Viewing all articles
Browse latest Browse all 144

Entity Framework Core 上で C# の文字列補間を使って安全に SQL 文を実行するが、SQL 文そのものを動的に組み立てたい

$
0
0
C# を使ったプログラミングにおけるデータベースアクセスにおいて、広く使われているデータベースアクセスライブラリ「Entity Framework Core」(以下 EFCore)。

EFCore を使ってのリレーショナルデータベース (例えば Microsoft SQL Server などのような) へのアクセスでは、まず滅多に生の SQL 文を C# ソースコード上に記述することはない。


C# ソースコード上では LINQ-to-Entities によるメソッド呼び出しでデータベースアクセスを実装し、EFCore がその内部にて、C# ソースコード上に表現された式を解釈して SQL 文を組み立ててデータベースエンジンに送信する仕組みだからだ。


しかし EFCore + LINQ-to-Entities も万能ではなく、どうしても SQL 文を文字列で指定して実行したい状況に迫られる場合もある。
例えば「指定した条件 (但し条件は実行時に決まる) に合致するレコードを一括して削除したい」といった例が挙げられる。
DELETE FROM FizzBuzz WHERE [Value] = 'Fizz' -- Value に対する条件は実行時に決まる
※実を言うと、一括削除なども C# の式木で安全に実装できる各種拡張ライブラリが流通していたりするので可能ならそれらを使うべきだ。しかし本記事では、それら拡張でも歯がたたないような状況を想定する。上記はあくまでも説明を簡単にするための例として読んでいただきたい。


だがしかし、実行する SQL 文を C# ソースコード上から文字列で組み立てて指定するとなると、恐ろしいのは SQL インジェクションだ。


幸い、EFCore では、SQL インジェクションの脆弱性発生の危険性を緩和してくれる機能が用意されている。
C# の "文字列補間" (String Interpolation) 機能を使った、ExecuteSqlInterpolatedAsync メソッドがそれだ。


このメソッドを使うと、以下のように実装できる。
var arg = "Fizz";
await dbContext.Database.ExecuteSqlInterpolatedAsync(
  $"DELETE FROM FizzBuzz WHERE [Value] = {arg}"
);

上記は一見、arg に "; DELETE  FROM VeryImprotantTable" みたいに書かれたら、凄く大事なテーブルが全件削除される SQL インジェクションが発生しそうにも見える。


しかし上記コードにおける $"..." の文字列補間 (String Interpolation) は、単純な単一の文字列に合成されるのではなく、「文字列書式とプレースホルダ位置および引数」のセットとして引き渡されるのだ。

ただの単一の文字列ではないので、EFCore 側でこれをちゃんとパラメタライズドクエリに組み立てて実行しており、SQL インジェクションには至らない仕掛けなのである。


このように SQL インジェクションの危険性を払拭しつつ、直感的で可読性の高いコードで、生の SQL 文の実行を実装できる便利機能である。


より高度に SQL 文を動的に組み立てる必要が発生したら?
しかし、実行したい SQL 文を、さらに高度に動的に組み立てる必要が発生したらどうなるか。
例えば上記例について、DELETE 対象のテーブル名も引数渡ししたい例を考える。


残念ながら、下記コードは実行すると (少なくとも Microsoft SQL Server に対しては「Must declare the table variable "@p0".」という) 実行時例外になる。
var tableName = "FizzBuzz";
var arg = "Fizz";
await dbContext.Database.ExecuteSqlInterpolatedAsync(
  $"DELETE FROM {tableName} WHERE [Value] = {arg}"
);

Microsoft SQL Server では DELETE 文における対象テーブル名はパラメータ指定できないからだ。


かといって下記もダメである。
var tableName = "FizzBuzz";
var arg = "Fizz";
await dbContext.Database.ExecuteSqlInterpolatedAsync(
  "DELETE FROM ["+ tableName + $"] WHERE [Value] = {arg}"
);
$"..." による文字列補間も、他の文字列との + 演算子との連結により評価・実行され、ただの文字列に成り下がってしまうからである。
つまり、文字列補間は「文字列書式とプレースホルダ位置および引数のセット」という高度な情報オブジェクトだったのに、その情報を ToString() して捨ててしまうようなものだ。


実際、そのような関係で上記コードはそもそも「error CS1503: Argument 2: cannot convert from 'string' to 'System.FormattableString'」というコンパイルエラーになってビルドが通らない。


解答例
ではどうするかというと、解答例はこうだ。
var tableName = "FizzBuzz";
var arg = "Fizz";
var sql = FormattableStringFactory.Create(
  "DELETE FROM [" + tableName + "] WHERE [Value] = {0}", 
  arg);
await dbContext.Database.ExecuteSqlInterpolatedAsync(sql);
実は、$"..." の文字列補間で生成されるオブジェクトの正体、「文字列書式とプレースホルダ位置および引数のセット」とは、FormattableStringFactory クラスのインスタンスであり、それは $"..." 構文のみならず、FormattableStringFactory.Create 静的メソッドによっても作り出せる、ということだ。


FormattableStringFactory.Create 静的メソッドによって、文字列書式部分とプレースホルダ位置、それらプレースホルダ位置に充てたい引数値、これらすべてを完全にプログラマの制御下で FormattableStringFactory オブジェクトを作り出すことができる。


そうして生成された FormattableStringFactory オブジェクトを EFCore は受け取って、パラメタライズドクエリとしてデータベースエンジンに発行できる次第。

まとめ
以上のように、 $"..." 構文だけに頼らず、FormattableStringFactory.Create 静的メソッドを利用することで、より高度に EFCore の ExecuteSqlInterpolatedAsync メソッドを活用することができる。


なお、本記事のような例であれば、EFCore および ExecuteSqlInterpolatedAsync メソッドに頼らずとも、もっと低レイヤの ADO.NET に基づいた実装で手組みでパラメタライズドクエリを発行することもできよう。
ただし本記事ではその点については深く立ち入らずに終わりとする。


なお、本記事の例では、結局は生の文字列演算による SQL 文生成が行なわれており、そこに SQL インジェクションの脆弱性が産まれる危険がある。


本記事の例で具体的にいうと、tableName 変数に設定されるテーブル名は、確実に決定論的に安全なテーブル名のみが指定され得るよう、厳重に注意する必要はある。


繰り返しになるが、本記事で例示した「指定した条件に合致するレコードの一括削除」は、あくまでも説明の都合で引き合いに出したまでである。
この例においては、実際には、EFCore 標準ではないもの、各種拡張ライブラリを導入したりすることで、生の SQL 文を組み立てることなく安全に実装・実行できるはずである。

本記事のような手法・技法は、本当に他の安全な・妥当な手段がない場合にのみ選択すべき実装の選択肢であろう。
その点はくれぐれも注意されたし。




Viewing all articles
Browse latest Browse all 144

Trending Articles