PEAK XOOPS - News in englishin japanese

Archive | RSS |
  
Poster : GIJOE on 2009-01-23 05:47:03 (18229 reads)

in englishin japanese
(3)で完成したと思っていたDBLayer Trapping anti-SQL-Injectionのロジックですが、これを実際にXOOPS 2.0.16a-JPでテスト運用してみたら、一般設定の更新でSQL Injectionの誤検出が発生しました。(具体的には、picoの共通ヘッダとしてダブルクオーテーションを含む文字列をポストする時)

このSQL対策ロジックは原理上、誤検出も当然あり得るのですが、どう見てもそういうパターンでもないので、疑問に思いながらXOOPSのソースを追ってみたら、かなりビックリするコードを見つけました。

class/database/mysqldatabase.php


    function quoteString($str)
    {
         $str = "'".str_replace('\\"', '"', addslashes($str))."'";
         return $str;
    }

ん? \"を"に書き戻している? なぜ?

ほとんどの文字列表現実装系で、シングルクオーテーションの内側でも"はエスケープの対象になると思いますし、実際、MySQLの文字列表現もそうなってます。なぜXOOPSのquoteString()がなんでこんな怪しいことをしているのか理解に苦しみますが、結果的に文字列のパースに間違いが出たこともなかったので、誰も突っ込まなかっただけなのでしょう。

いずれにせよ、このメソッドの正解は以下のコードです。

    function quoteString($str)
    {
         $str = "'".str_replace('\\"', '"', addslashes($str))."'";
         $str = "'".mysql_real_escape_string($str)."'";
         return $str;
    }

MySQL用のDBレイヤーなのですから、mysql関数が堂々と使えますし、逆に、MySQLのクライアントエンコーディングがSJISやBIG5にならない、なんて判断はこのメソッドには出来ないわけですから。

ふと気になって、ImpressCMSと本家版2.3.2を調べてみたら、どちらもちゃんとmysql_real_escape_string()を使う実装になってました。おそらくその変更タイミングは、本家版の2.0.14〜17あたりでしょうか。

つまり、XOOPS Cubeプロジェクトだけが取り残されている状況だったんですね

ただ、いずれのコアを使う場合であっても、今回と同様の誤検出は考えられるので、Protector-3.3.1では、以下のようなロジックを追加導入しました。

「'や"を含んだリクエストが、SQL内にエスケープされていない状態で含まれていたとしても、そのリクエスト全体が、SQLの文字列内に収まっている(=クオーティングを破っていない)ならSQL Injectionではない」

これで誤検出そのものも減りそうです。

0 comments

Poster : GIJOE on 2009-01-16 05:07:17 (15889 reads)

in englishin japanese
DBレイヤーのオーバーライドの準備ができたとしても、リクエストとすべてのSQLを比較するのは、それなりにコストが高い手法であることを自覚する必要があります。

そこで、まずはDBレイヤーをオーバーライドする頻度を下げる努力をします。

ほとんどのリクエストには「攻撃となりえる」文字列は最初から含まれていないのですから、その時には元のDBレイヤーをそのまま使います。
あくまで、特定の文字列が含まれている時だけ、DBレイヤーをオーバーライドすることで、速度と安全性を両立できるでしょう。

もちろんその「特定の文字列」の判断こそが難しいのですが、こんなルールにしてみました。


/(information_schema|select|'|")/i

ここでは、引用符を壊す可能性のある'および"、そして、他のテーブルを参照する可能性のあるselectのみを検出しています。
unionは単独では意味を持たないのでここでは考慮しません。
また、コメント開始記号は誤認識が多い上(特に $_SERVER['HTTP_ACCEPT'] に*/*が含まれることが多い)、引用符を壊してからでないと攻撃としてもほぼ意味がないので、特定の文字列には含めません。


さて、SELECTや'や"を含んだ、「攻撃となりえる」リクエストを検出して、DBレイヤーのquery()メソッドをオーバーライドしたとしましょう。

次は、query()メソッドのなかで、SQL Injection攻撃の可能性があるリクエストと、実際に発行しようとしているSQLを比較します。

SQL Injection脆弱となるパターンには、大きく分けて2つあります。

(1) リクエストがクオーテーションの内側に収まるが、エスケープし忘れたもの
例)
SELECT ... FROM `table` WHERE `column`='(エスケープし忘れた文字列)'

(2) リクエストが最初からクオーテーションの外側にあるもの
例)
SELECT ... FROM `table` WHERE `integer_column`=(リクエスト)
SELECT ... FROM `table` WHERE ... ORDER BY (リクエスト)

ほぼ確実に防げるのは(1)のパターンです。

具体的には、リクエストの中で、シングルクオーテーションやダブルクオーテーションを含む文字列を記憶しておき、その文字列がそのままSQLに含まれるようなら、SQL Injectionだと判定してしまいます。というのも、まともなPOST文字列などであれば、クオーテーションの内側に収まっているはずなので、エスケープされていなければならないからです。

'や"を含む文字列をリクエストされた時に、たまたまリクエスト非依存のSQLと、偶然に一致してしまって、SQL Injection対策が効きすぎてしまう恐れはありますが、これはチェックすべきリクエスト文字列の最低字数を長くすることで可能性を下げることができます。さじ加減が難しいところではあるのですが、ギリギリ6文字未満の文字列は見逃す、くらいのバランスであれば、悪意あるSQL Injectionとしては有効な攻撃にならず、かつ、誤検出もほとんどでないでしょう。

このパターンでの最短と思われる有効な攻撃文字列はこうですから。
(もっと短いのがあったら教えてください)

'OR 1#


逆に、(2)のミスをカバーしきるのは、かなり苦しいと言わざるを得ません。(2)のようなSQL Injection脆弱性がある時点で、そのtableのデータが読み書き自在とされるのは諦めるべきでしょう。

ただ、SQLとリクエストとの比較ロジックで頑張れば、他のテーブルへの波及はギリギリ防げると思います。

具体的には以下の2つの方法の併用です。

(A) SQL内にコメントがある時点でアウトとする
(B) SQLから文字列部分を取り除いたものの中に、SELECT等を含むリクエスト文字列が存在すればアウトとする

(A)はやりすぎな気もするのですが、現実にコメント付きのクエリを発行することはまずありませんし、怪しいリクエストが存在した時しかこのロジックは効かないので、ほぼ問題にならないだろう、と判断してます。

(B)がこのロジックの肝です。サブクエリにしてもUNIONにしても、基本はSELECTをどう埋め込むかです。そのSELECTを含んだリクエスト文字列が、クオーテーションの内側にない、という時点でアウトと判断できれば、他のテーブルに対するほとんどの攻撃を防げるでしょう。(もちろん、テーブル名一覧をinformation_schemaから取得する攻撃も)

このあたりの具体的な実装は、Protector-3.3 のProtectorMySQLDatabase.phpを確認してもらうのが良いと思います。

0 comments

Poster : GIJOE on 2009-01-15 16:12:57 (15470 reads)

in englishin japanese
あっという間に1週間経ってしまいましたが、「リクエスト層とDB層の両方を比較して、SQL Injectionを防ぐ方法」の続きです。

PHP汎用の話題であれば、何らかのフレームワークにこれを組み込む、という話になるのでしょうが、今どきのフレームワークはORマッパーを使うものが多く、クエリを直接発行することもないでしょうから、例によってXOOPS用モジュールであるProtectorにどう組み込むか、という話になります。

まず、DBレイヤーをフックする必要がありますが、XOOPSの場合、ここが非常に硬直化していて、後からDBインスタンスを乗っ取ることはほぼ不可能です。つまり、いずれのXOOPSコアについても、データベースファクトリクラスを書き換える必要があります。

フック方法もいろいろ考えたのですが、絶対にリクエスト依存にならない、という理由から、定数を利用することにしました。最初に、XOOPS_DB_ALTERNATIVEという定数が宣言されていれば、そのクラスがDBインスタンスとして用意されます。

class/database/databasefactory.php


			require_once $file;
			/* patch from */
			if ( defined('XOOPS_DB_ALTERNATIVE') && class_exists( XOOPS_DB_ALTERNATIVE ) ) {
				$class = XOOPS_DB_ALTERNATIVE ;
			} else /* patch to */if (!defined('XOOPS_DB_PROXY')) {
				$class = 'Xoops'.ucfirst(XOOPS_DB_TYPE).'DatabaseSafe';
			} else {
				$class = 'Xoops'.ucfirst(XOOPS_DB_TYPE).'DatabaseProxy';
			}
			$instance =& new $class();

これはこれで融通の効かない仕組みではあるのですが、DBインスタンスを直に書き換えるメソッドを追加する、なんてのも危険な気がしたので、各コアチームが採用しやすい形を最優先に考えた結果、こうなりました。

Protector-3.30にはそのパッチも含めたので、ぜひとも採用の検討、お願いします。>minahitoさん・marcanさん・phpppさん

次回はオーバーライドする条件および、比較ロジックについて書きます。


Poster : GIJOE on 2009-01-09 13:09:35 (19616 reads)

in englishin japanese
世界的にXOOPSの本家とは xoops.org ってことになり、本流は2.3系列って事になっているようです。

そこの最新版である xoops-2.3.2b というアーカイブをダウンロードしてみてあきれました。

htdocs/ の中に、xoops_lib というフォルダがあって、その中に、XOOPS_TRUST_PATHに置かれるべきファイルが詰まっているのですから。
しかも.htaccessすらなし。

思わず目が点になりました。

mamba氏が、XOOPS_TRUST_PATH内のファイルにLFIがある、という不思議なレポートをしてきて、その意味が理解できなかったのですが、アーカイブ構造を見て納得しました。
つまり、彼らはXOOPS_TRUST_PATHの意味を一つも理解していないのです。
XOOPS_TRUST_PATHを、xoops_lib なんて名前に勝手に変更しているのも、彼らがXOOPS_TRUST_PATHの意味を理解していないことの証拠の一つでしょう。

mamba氏は「Protectorにセキュリティホール(LFI)があるから独自にfixをした」、とか言ってましたが、DocumentRootの外にあるのが前提となっているファイル群に場当たり的なパッチを当ててもまったくの無意味です。

案の定、今度はRFIとしてレポートが上がったそうです。
http://www.milw0rm.com/exploits/7705

これはProtectorの穴ではなく、XOOPS-2.3.2の穴であると正確に理解してください。

とにかく、phppp はじめ xoops.org の開発者に言いたいことは以下の3つです。
XOOPS_TRUST_PATHをアーカイブのhtdocs/の外に出してください。
DocumentRootの外と内の違いについて勉強してください。
Protector V3のインストール方法を繰り返し読んでください。

それが出来ないのなら、同梱なんてしないでください。

誤ったインストール方法をデフォルトで用意しておいて、Protectorにセキュリティホールがあるなんて言ってくるxoops.orgの神経が信じられませんし、それが原因で第三者にProtectorの存在意義を問われてしまうのもこれまた心外です。


Poster : GIJOE on 2008-12-28 18:02:01 (8711 reads)

in englishin japanese
先日、gusagiさんより、bulletinHDについてのバグレポートとして、以下のURLを転送してもらいました。
http://ryus.co.jp/modules/d3blog/details.php?bid=107
先に結論から書くと、このエントリはMySQLの基本的なcase sensitiveルールを誤解していることによる、間違ったレポートです。
Quote:


この現象は、 Windows環境で作成したDBのデータをUNIX環境に持って行った場合に限定されます。

この時点でバグでも何でもありません。まずはこっちを読んでください。
http://dev.mysql.com/doc/refman/4.1/ja/name-case-sensitivity.html
Quote:

ただ、WARPなどの環境でデータを作成してから、UNIX環境にデータを移行して運用なんてケースも考えられるので、その場合は上記パッチを適用して頂いた方が良いと思われます。

そういう「仕様外」のことをやりたければ、lower_case_table_names=1 を使うべきでしょうし、もしなんらかの理由でソースコードを改変するしかない場合でも、Database::prefix()の方にかければ一箇所で済みます。SQL生成部すべてにstrtolower()をかける、という方法はメンテナンス性という観点からも筋が悪すぎます。

# 個人的には、Case sensitiveな環境がターゲットなのに、Case insensitive環境で開発しているっていうのがあり得ない…


ただ、このレポートのおかげで私も一つ勘違いしていたことに気付きました。
それは、caseしか違わないdirnameで、2つのモジュールが同時には動かせない、ということです。

当たり前ですが、case sensivie環境で動かすのなら、XOOPSにおけるdirnameも原則的にcase sensitiveです。例えば、XUGJのQandAフォーラムは、あくまで"QandA"であり"qanda"ではありません。(中身はd3forum)

http://www.xugj.org/modules/QandA/
上のリンクは生きてますが、
http://www.xugj.org/modules/qanda/
下のリンクは死んでます。

「つまり、D3モジュールでありさえすれば、picoがインストールされている環境でも、PICOというdirnameで別のモジュールとしてインストールできるだろう」

ここに思い込みがありました。

実際にはcase sensitiveな環境でもインストールできません。
なぜならSQLとして以下のクエリで判断されるからです。

SELECT (snip) WHERE dirname='PICO'

この 'PICO' はcase insensitiveに比較され、'pico'と一致します。

つまり、インストール済だよ、というメッセージで拒否されます。当たり前ですが、インストーラだけを無理矢理通過させても意味がありません。やはり同様のクエリによって、モジュールの分岐処理がなされるからです。

では、どうすれば良いのか。答えは簡単です。

ALTER TABLE (prefix)_modules MODIFY `dirname` varchar(25) binary NOT NULL default '';

としてあげれば良いのです。dirnameカラムにbinary属性を加えることによって、case sensitiveになりました。

これを実際に試してみると、picoとPICOが普通に共存できました。
まあ、そんな需要もほとんどないとは思いますが、ちょっとしたTipsくらいに覚えておくと、今後役に立つことがあるかもしれません。

なおこの場合、言語定数オーバーライド機能の一部が、picoとPICOでは独立して働きません。
それは、

$constpref = '_MI_' . strtoupper( $mydirname ) ;

というコードによるものです。

考えてみたら、定数名が大文字である必然性などどこにもなく、strtoupper() を$dirnameにかけることは、バグ以外の何物でもないでしょう。

今後のD3モジュールでは、$constprefへのstrtoupper()を外していきます。(各種言語ファイルに含まれてしまっているのがかなり面倒だったりしますが…)

0 comments

« 1 (2) 3 4 5 ... 37 »
Login
Username or e-mail:

Password:

Remember Me

Lost Password?

Register now!