こんにちは。セキュリティ技術グループのはるぷと申します。
不正なJSON形式を利用したSQL Injectionの技術情報が公開され、一部のPerlモジュール(SQL::Maker)において対応策が導入されたことを受けて、本BlogでもJSON形式を利用したSQL Injectionの技術詳細について解説します。
1. JSON SQL Injection
通常、SQL Injectionというと、「' or 1=1 -- 」という王道パターンや、Blind SQL Injectionといった情報を細かく引き出すパターン、マルチバイトを利用した複雑なものなどが良く知られていますが、今回は、PerlにおけるJSON形式を利用した特殊ケースにおけるSQL Injectionについて紹介したいと思います。
JSON形式を利用したSQL Injectionとは、大まかには、「プレースホルダを利用したSQL Injection対策をJSON形式のパラメータを利用して回避する」、というものになります。
※以降、この事象について「JSON SQL Injection」と呼ぶこととします。
ここで書かれる内容には、対策の実施要否の判断を助けるためにSQL Injectionが発生する原理の解説が含まれています。JSON SQL Injectionによって不正に情報を引き出すなどの直接的なパターンは記載していないため、そのまま不正アクセスに利用することはできませんが、他人のプログラム、サーバへの試行や、悪用は絶対に行わないでください。影響の有無を確かめる際は、自分自身の管轄するプログラムの動作確認にのみ利用してください。
2. JSON SQL Injectionとは
JSON形式を利用してSQL Injectionを行う、といった場合、「{"name":"value"}」のようにJSONパラメータの値(value)の箇所に「'or 1=1-- 」等のSQL文を指定することを想像されるかもしれません。そのケースの場合、プレースホルダなどを利用して正しく値がエスケープされる仕組みを導入していれば、SQL Injectionの観点では、非JSONの場合の挙動と変わる部分は特に無いため問題とはなりません。
しかし、ここで説明するJSON SQL Injectionは、パラメータの値ではなく型により解釈が異なってくるケースで発生するものになります。
特定のメソッドの挙動を制御する際に、引数の型や数を変更することで、同じメソッドに対して異なる挙動を定義する手法についてプログラミングにおいてはしばしば利用されることがあります。通常、そのような利用方法を行う際には、予め定義されている型に沿ったコードになるケースが多く、通常問題となることは少ないと考えられます。
しかし、ユーザからの入力値など、外部からの入力に対する処理において、入力値に複数の型が利用できる仕組みを利用している場合(かつプログラム側で型を明示的に指定する必要がない場合)、型によって処理が変わる挙動が問題になることがあると考えられます。
Perlでは、スカラ変数として、数値、文字列、リファレンスの3種類の型を持っていて、Json.pmを利用してJSONを処理すると、この3種類のいずれかの値として変換を行います。「a=b&c=d」の形式のパラメータの場合、数値や文字列の2種類になるため、「リファレンス」の部分が大きく異なります。
「リファレンス」では、配列やハッシュ(連想配列)への参照を扱うことができ、JSON形式で考えた場合、以下の部分が相当します。
#JSON形式の例:
{ "name" : "value", ------ 文字列として扱われる "hash_ref" : {"name" : "value"}, ------ ハッシュのリファレンスとして扱われる "array_ref" : ["value1","value2","value3","value4"] ------ 配列のリファレンスとして扱われる }
JSON SQL Injectionでは、上記の「文字列」の形式で定義された値に、ハッシュのリファレンス(hashref)や配列のリファレンス(arrayref)を指定することで誤動作が起きることで発生します。以下にSQL::Makerでこの事象が起きるケースについてサンプルを示します。
#サンプルコード
use strict; use SQL::Maker; use JSON; #ユーザ入力の取得 my $json = "ユーザの入力";# {"name":"xxxxxx"}のような形式を想定 my $user_name = decode_json($json)->{"name"}; #SQL文の生成 my $builder = SQL::Maker->new(driver => 'MySQL', quote_char => '`'); my ($sql, @bind) = $builder->select( "table_name", ["*"], {"name"=>$user_name}); print "SQL文: ".$sql."\n"; print "変数: ".join(",",@bind)."\n";
このサンプルコードでは、ユーザの入力に想定される文字列としては、{"name":"xxxxxx"}といった形式のものになっています。想定される結果としては、SQL文およびBind変数に関しては以下のようなものが生成されることとなります。
SQL文: SELECT * FROM `table_name` WHERE (`name` = ?) 変数: xxxxxx
例えば、この文字列形式でSQL Injectionを意図的に引き起こそうとする場合、通常{"name":"xxxxxx' or 1=1 -- "}のように値の部分に不正なSQL文の指定を試みることが考えられます。しかし、SQLクエリビルダーによって安全なSQL文が生成されるため、下記の様に指定したSQL Injectionのパターンが変数として扱われSQL文となることはなく、SQL Injectionが起きない仕組みになっています。
SQL文: SELECT * FROM `table_name` WHERE (`name` = ?) 変数: xxxxxx' or 1=1 --
正しくプレースホルダが利用されていて、変数にも想定しているものが指定されている状態となっていて、特に問題となる部分はありませんね。
しかし、ユーザ入力として指定される文字列が、想定していないJSON形式だった場合に問題となるケースが存在しています。上記のサンプルコードでは、入力をJSON形式として解析を行い、取得したnameパラメータの値をそのまま利用する形になっています。そこで、下記のようなJSON形式の入力を行った場合を考えます。
{"name":{"test":"bar"}}
JSON形式としては正しいものになりますが、Json.pmを利用してパースを行った場合、nameパラメータの値は入れ子になったパラメータの取り出しが可能となるように、ハッシュのリファレンスとなります。この入力が指定された状態で、サンプルコードを実行すると以下の様になります。
SQL文: SELECT * FROM `table_name` WHERE (`name` TEST ?) 変数: bar
where句に「`name` TEST ?」という不正な演算子が指定されてしまっていて、SQL文として成り立たなくなってしまっています。このSQL文をそのまま実行すると、例えば以下のようなSQL エラーとなります。
DBD::mysql::st execute failed: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'TEST 'bar')' at line 3 at sql_maker.pl line 35.
ここでは攻撃として成立する具体的なパラメータの例示は避けますが、ユーザ入力によりSQLを破壊することができてしまっており、結果としてSQL Injectionが可能な状態になっています。
これは、SQL::Makerクエリビルダー側の、where句の値にhashrefが指定された場合、指定された連想配列の1つ目を演算子として利用できるという仕様に基づいた結果になります。 同様の事象は他のSQLクエリビルダーでも発生する可能性があり、SQLクエリビルダーによってはハッシュのリファレンスだけでなく配列のリファレンスもJSON SQL Injectionを引き起こす形式となります。また、selectだけでなくinsertなど他の形式のクエリでも同様の事象が発生するSQLクエリビルダーも確認されています。
ここでは、SQL::Maker 1.15を例として利用していますが、同様にリファレンスを受け取った場合に処理内容が変わる仕様を持つモジュールを利用する際には、注意が必要になります。
また、上記の様にSQLの構文が壊れない場合でも、問題となるケースも存在します。例えば、下記の様に値の部分を配列として与えるとSQLの構文が変化します。
JSON:{"name":["test","bar"]} SQL文: SELECT * FROM `table_name` WHERE (`name` IN (?, ?)) 変数: test,bar
配列のリファレンス(arrayref)を値として指定した場合、where句の演算子が「IN」になり(OR演算子での連結となるSQLクエリビルダーもあります)、指定した配列のいずれかに条件がマッチした場合に結果が得られる形になります。これが、例えばログインのような箇所で発生した場合、ユーザ名を列挙するなどして特定のパスワードに一致するユーザを一度に大量に試行することが可能になるなどのケースも想定しえる状態になります。
2. 対策
SQL::Makerの場合、最新版(執筆時最新版は1.17)にて、代替手段が用意されているため、そちらを利用するのが望ましいものとなります。
#対策例1 (strictモードの利用):
my $builder = SQL::Maker->new(driver => 'MySQL', quote_char => '`', strict => 1);
#実行結果(エラー):
cannot pass in an unblessed ref as an argument in strict mode at /path/to/perl_lib/SQL/Maker.pm line 224.
また、類似のSQLクエリビルダーにおいて、JSON形式の入力により誤動作をするケースがいくつか確認されています。その場合、上記のような対応は難しいため、代替手段が用意されていない場合は、入力された値の型をチェックする、または型を変換することが効果的と考えられます。
#対策例2 (型チェック):
my $name = decode_json($json)->{"name"}; if(ref($name)) { #エラー処理 exit; }
#対策例3 (変換):
my $name = decode_json($json)->{"name"} . ""; #空文字の追加(文字列への変換)
※本挙動は、あくまでも異なるモジュール(今回の場合はJson.pmとSQLクエリビルダー)の仕様の組み合わせによる例外的な挙動であるため、SQL Injectionの対策方法についてプレースホルダを利用すべきかバリデーションで対処すべきかという一般論に影響を与えるものではありません。
JSON形式でパラメータの授受を行うアプリケーションは、近年増えてきていると考えられます。特に、WebアプリケーションでのXMLHttpRequestでの利用や、スマートフォンアプリでのサーバとのデータ通信等で頻繁に利用されてきています。もし、受け取ったJSON形式のパラメータについて、検証が不十分と考えられる場合、一度想定外のJSON形式を受け取った場合に誤動作をしてしまわないか確認することをお勧めします。
3. 本挙動の発見、報告の経緯について
本挙動については、弊社検証環境上で作成したアプリケーションのテストを実施している際に、本挙動を確認するに至りました。
情報の公開に際して、JSON形式を利用したSQL Injectionが発生するケースを、特定のperlモジュールに関してDeNAよりIPAへ届け出を行いました。しかし、そもそもモジュールの仕様として定義されている挙動であり、arrayrefやhashref等の入力に対する処理としては正しい物となります。その結果、本件についてはIPAの脆弱性関連情報届出制度では脆弱性としては扱わず、不受理となっています。
Json.pmと組み合わせることによって想定していなかった動作とはなっていると考えられますが、それぞれのモジュールの動きとしては単体では脆弱性と言える動きにはなっていませんでした。従って、この挙動自体をモジュール側の脆弱性と断言するのが難しい状態と考えられます。
しかし、本挙動によって潜在的にSQL Injectionの脆弱性を抱えるアプリケーションが存在する可能性があり、現状では該当する挙動についてはアプリケーション側で個別に対応していく必要があります。本記事によってSQL Injection対策の浸透に貢献できればと考えています。
4. 最後に
脆弱性情報について正しく理解して、正しく対応・対策を行いましょう。ご不明な点やお問い合わせがありましたら、お気軽にコメントやMobageオープンプラットフォーム事務局までご連絡ください。(パートナー様はこちらから)
長文を読んでくださり、ありがとうございました!