クライアントから直接SQLを送る以上、セキュリティには気を使っていきたいと思います。
クライアントからSQLを送るということは、断片的に渡すこととは、リスクが根本的に異なり、対策も通常の方法とは違ってくるので簡単ではありません。
私の知らないことや、気づかないことも多々あると思います。恥ずかしながら、ソースは丸ごと公開していますので、ぜひご指摘いただけると有難いです。
【→bbs】
ただ、SQL文を丸ごと渡せるといっても、JavaScriptとデータベースが直結しているわけではありません。
間に各DBへクエリを振り分ける、ゲートウエイのPHPが置かれて、エスケープやフィルタリング、IP許可なども行います。
(機能を満たせばPHPである必要はありませんが、多くの人が理解できて、かつWebセキュリティ処理のメンテナンスも早いのでPHPを選択しています。)
目指したい方向としては、危険だから止めてしまうのではなく、問題点をたとえば、ゲートウエイ内で汎用的に解決することなどで、
逆に個々の制作時にはほとんど悩む必要も、サーバーに触る必要も無くなる?という具合になると良いなぁ、、、と思っています。
だって、今振り返って、考えてみれば、ApacheだってOfficeだってLinuxだってWindowsだって、NetscapeもIEも、SSLもSSHさえも、
何度も修正してきたじゃないですか、、、。<って相手が大きすぎ。
現状では、テストバージョンですので、インジェクション対策やその他もろもろの処置をコメントアウトしたり、修正中だったりする場合があります。従って、現状のバージョンを利用される際は、充分に、その点をご理解いただいた上でお願いします。修正せずに放置することは危険ですのでご遠慮下さい。安定バージョンは近々出します。
権限
JavaScriptからSQL文を発行するということは、INSERTでもUPDATEでもDELETEでもDROP でも渡せてしまうということです。
しかし、だからといってそれらが、誰にでも使えるというわけではありません。
AjaSQLは、第一に
ゲートウエイ側での、.allow_ip、.allow_db_table、.allow_column 等の設定ファイルによる許可を予定しています。特に、.allow_db_tableは、この許可がなければ、リクエストされたDBとテーブルへのアクセスができなくなります。(<次期バージョンからです。書き込み読み込み許可をどうするかは検討中)
AjaSQLは、第二に
各データベースの権限システムに従います。SQLiteならOSパーミッションによる権限、
MySQLなら、ユーザー/パスワードとGRANT/REVOKEなどによる各権限もそのまま有効です。
しかしながら、せっかくの権限システムがあっても、他のCGIやPHPなどと同様に、Webサーバー経由でのデータベース処理では、
ユーザー権限を不特定多数で共有せざるを得ないため、多くのリスクを抱えてしまいます。
したがって、特別な理由がなければ、CREATEやDROPなどの強力な権限をWebユーザーに与えるべきではないと思います。
重要な作業はスーパーユーザーがコマンドラインから行う方法を推奨します。
それでも、Webアプリの多くは、SELECT、INSERT、UPDATEあたりのコマンドで作ることが可能です。
AjaSQLの権限は、いまのところ、
デフォルトですべてを透過するのではなく、それらを明示的に許可するようにしたいと考えています。
AjaSQLの権限は、これらの組み合わせで指定することになります。
SQL/OSインジェクション
クオート
たとえば、次のようなコードを書くと、
シェルコマンドのリダイレクトが実行されてtestFile1とtestFile2が作成されてしまいます。
<?php
//////
// OSインジェクションテスト 失敗例
//
//変数
$asql_dbName = './mydb_bbs.db > testFile1';
$asql_sqlStr = 'SELECT * FROM mydb1 > testFile2';
//SQLite実行
$data = asql_getResult_sqlite($asql_dbName,$asql_sqlStr);
//結果表示
echo ($data );
function asql_getResult_sqlite($asql_dbName,$asql_sqlStr)
{
$sql = "sqlite -list -separator ',' $asql_dbName $asql_sqlStr";
return `$sql`;
}
?>
とりあえず、これを防ぐには、各変数をクオートで括るのが有効です。
$sql = "sqlite -list -separator ',' '$asql_dbName' '$asql_sqlStr'";
しかし、PHPには、もっと有効な
シェル引数として使用される文字列をエスケープしてくれるescapeshellarg()とescapeshellcmd()関数があるので、これを利用してみます。
ただし、escapeshellcmd()は、SQL文に使うとSQL文ではなくなってしまうので$asql_dbNameだけに適用します。
<?php
//////
// OSインジェクションテスト
//
//変数
$asql_dbName = './mydb_bbs.db > testFile1';
$asql_sqlStr = 'SELECT * FROM mydb1 > testFile2';
//SQLite実行
$data = asql_getResult_sqlite($asql_dbName,$asql_sqlStr);
//結果表示
echo ($data );
function asql_getResult_sqlite($asql_dbName,$asql_sqlStr)
{
//OSインジェクション用エスケープ
$asql_dbName = escapeshellarg($asql_dbName);
$asql_dbName = escapeshellcmd($asql_dbName);
$asql_sqlStr = escapeshellarg($asql_sqlStr);
$sql = "sqlite -list -separator ',' $asql_dbName $asql_sqlStr";
return `$sql`;
}
?>
この結果、 $sqlの中身は、下記のようになりシェルコマンドは実行されません。
sqlite -list -separator ',' './mydb_bbs.db \> testFile1' 'SELECT * FROM mydb1 > testFile2'
しかし、ここで、もし、$asql_dbName と $asql_sqlStrをクオートすると、どうなるでしょう?
sqlite -list -separator ',' ''./mydb_bbs.db \> testFile1'' ''SELECT * FROM mydb1 > testFile2''
このようになってtestFile2だけは作成されてしまいます。というわけで、現在は、$asql_dbName にescapeshellarg()、escapeshellcmd()
、$asql_sqlStrにescapeshellarg()を適用し、$asql_dbName と $asql_sqlStrをクオートしないというパターンを使っています。
ただ、そうすると、$asql_sqlStr側へなんらかの方法でシェルコマンドを注入される心配が残っています。次にそれを確認します。
バッククオートをエスケープ
$asql_dbName = str_replace('`', '%60', $asql_dbName);
$asql_sqlStr = str_replace('`', '%60', $asql_sqlStr);
シェルコマンドをピンポイントエスケープ
上記のOSインジェクション用エスケープによるシェルコマンド防御は、$asql_sqlStrに対しては、まだ穴があります。
SQL演算子などとescapeshellcmd()がエスケープするコマンドの区別がつかないため、$asql_sqlStrには使っていません。したがって、「;」とともに
注入された場合、シェルコマンドは、まだ生きていることになります。
本来は、SQL構文をパースし、しっかり仕分けしたうえでエスケープするべきだと思いますが、
取り合えず、今は、みっともないですが、、、SQL側を犠牲にして「|」「>」「&」を、さらに「`」も強制エスケープしてあります。(_ _b
確認したら、$asql_sqlStrにescapeshellarg()を適用後の下記のケースでもtestFile2は作成されませんでしたので解除します。どうでしょうか?
sqlite -list -separator ',' '../../../mydb_bbs.db \; \> testFile1' 'SELECT * FROM mydb1 LIMIT 5 ; > testFile2'
セミコロン以下を無視
複文の使えるデータベースなら、下記のように「;」でつなげて好きなSQL文を発行できますが、「;」以降を無視することで、
複文禁止とします。もっとも、たとえば、INSERTを
権限 によって対処できますが、
上記の、シェルコマンドエスケープ等の処理でOSインジェクションの処置が足りなかった場合の安全策としても押さえておきたいと思います。
また、INSERTを許可したユーザーからの連続登録攻撃などの対策なのについては別に立てたいと思います。
$asql_sqlStr = "SELECT * FROM mydb1 LIMIT 5 ;INSERT INTO mydb1 VALUES( Null ,'select_200_2_0_s', '' ,'' ,'', '')";
AjaSQLの応答速度は、単文接続使い捨てでも充分に使えるレベルだと思いますし、、、。
実装
ajasql_gw234.phpでは、システムへ渡す直前の
$asql_dbNameは、escapeshellarg()とescapeshellcmd()でエスケープ。
$asql_sqlStrは、バッククオートをURIエスケープ( ajasql_ajasqlxxx.jsライブラリ側でデコードします )。
$asql_sqlStrは、更に「>」「|」「&」もURIエスケープ( ajasql_ajasqlxxx.jsライブラリ側でデコードします )。
$asql_sqlStrは、更に「;」以降へコマンドを挿入された場合に備えて、「;」以降を無効にしています。
$asql_sqlStrは、更にescapeshellarg()でエスケープ。
$asql_sqlStrは、更にsqlite_escape_string()でエスケープ<--不要?。
// for SQLite
function asql_getresult_sqlite($asql_dbName,$asql_sqlStr)
{
//バッククオート等とりあえずエスケープ、、、うう、、、
//SQL演算子が使えなくなるので、まじめにSQLパースしてみるべきか、、、
$asql_dbName = str_replace('>', '%3E', $asql_dbName);
$asql_sqlStr = str_replace('>', '%3E', $asql_sqlStr);
$asql_dbName = str_replace('|', '%7C', $asql_dbName);
$asql_sqlStr = str_replace('|', '%7C', $asql_sqlStr);
$asql_dbName = str_replace('&', '%26', $asql_dbName);
$asql_sqlStr = str_replace('&', '%26', $asql_sqlStr);
$asql_dbName = str_replace('`', '%60', $asql_dbName);
$asql_sqlStr = str_replace('`', '%60', $asql_sqlStr);
//特殊文字除去 複文やOSインジェクション禁止(;以降に何があっても無視)
$asql_sqlStr_esc_chars = split(";",$asql_sqlStr);
$asql_sqlStr_esc_char = $asql_sqlStr_esc_chars[0];
//SQLインジェクション用エスケープ 不要かも
$asql_sqlStr_esc_char_sql = sqlite_escape_string($asql_sqlStr_esc_char);
//OSインジェクション用エスケープ
$asql_dbName_esc_os = escapeshellarg($asql_dbName);
$asql_dbName_esc_os = escapeshellcmd($asql_dbName_esc_os);
$asql_sqlStr_esc_char_sql_os = escapeshellarg($asql_sqlStr_esc_char_sql);
//$asql_sqlStr_esc_char_sql_os = escapeshellcmd($asql_sqlStr_esc_char_sql_os);
return `sqlite -list -separator "," $asql_dbName_esc_os $asql_sqlStr_esc_char_sql_os `;
}
MySQL
// for MySQL
function asql_getResult_mysql($asql_dbName,$asql_sqlStr)
{
//中断中
}
sqlite_escape_string()のテスト
escapeshellarg()のテスト
escapeshellcmd()のテスト
クロスサイトスクリプティング
未実装ですが、デフォルトでタグ無効(2005-12-10 ajasql_gw233.phpへ実装)。HTMLタグはデフォルトで、
すべてサーバー側(現在は出力時点)で<を<へ変換する方法により無効化。有効にしたい場合は、JavaScriptを書く側のWeb制作者
、またはブラウジング中に、ユーザー自らが(たとえばブックマークレットなどで?)、明示的に逆変換する。
DBのユーザー名とパスワード
MySQLなどのユーザー名とパスワードはJavaScript側には置かない。Webディレクトリにも置かない。暗号化するべき。
SQLite DBファイルのパーミッション
デフォルトは書き込み禁止を推奨。DBファイルのパーミッションに注意が必要。0604にすれば、書き込み不能になります。
また、DBファイルの所有者がWebサーバーのユーザー(nobodyやapacheなど)にならないよう注意する。WebサーバーでDBファイルを作らないこと。
不正な連続クエリ
連続クエリの回数とインターバルをGWあたりにセット予定。
Same Origin Policy
参考リンク:
→IPA セキュアデータベースプログラミング
→PHP マニュアル SQLインジェクション
→[ThinkIT] インジェクション攻撃