EC-CUBE におけるクロスサイトリクエストフォージェリの脆弱性について

5月10日, JVN より EC-CUBE における CSRF脆弱性について発表がありました.

JVN#37878530: EC-CUBE におけるクロスサイトリクエストフォージェリの脆弱性

2.11.0 では対応されているけど, 2.4.x ではどうなの?? という声があがっていますので, ちょっと書いておこうと思います.

対策方法は, id:xross-cube さんところに, 詳しく書かれていますが, おそらく 100ヶ所以上の修正を入れなくてはならないため, 一筋縄にはいかないようです.

EC-CUBE2.4.4以前のバージョンに存在するクロスサイトリクエストフォージェリの脆弱性への対応 - EC-CUBEのカスタマイズ、ネットショップ制作メモ

また, JVN には,

管理者が、EC-CUBE にログインした状態で悪意あるページを読み込んだ場合、当該製品で管理している情報を改ざんされる可能性があります。

と書かれていますが, 重要な情報が抜けています.

EC-CUBE の管理画面では SC_Utils::sfIsSuccess() 関数で認証を行っていますが, ここで HTTP_REFERER のチェックをしています.

http://svn.ec-cube.net/open_trac/browser/branches/version-2_4/data/class/util/SC_Utils.php#L208

<?php
    /* 認証の可否判定 */
    function sfIsSuccess($objSess, $disp_error = true) {
        $ret = $objSess->IsSuccess();
        if($ret != SUCCESS) {
            if($disp_error) {
                // エラーページの表示
                SC_Utils::sfDispError($ret);
            }
            return false;
        }
        // リファラーチェック(CSRFの暫定的な対策)
        // 「リファラ無」 の場合はスルー
        // 「リファラ有」 かつ 「管理画面からの遷移でない」 場合にエラー画面を表示する
        if ( empty($_SERVER['HTTP_REFERER']) ) {
            // TODO 警告表示させる?
            // sfErrorHeader('>> referrerが無効になっています。');
        } else {
            $domain  = SC_Utils::sfIsHTTPS() ? SSL_URL : SITE_URL;
            $pattern = sprintf('|^%s.*|', $domain);
            $referer = $_SERVER['HTTP_REFERER'];

            // 管理画面から以外の遷移の場合はエラー画面を表示
            if (!preg_match($pattern, $referer)) {
                if ($disp_error) SC_Utils::sfDispError(INVALID_MOVE_ERRORR);
                return false;
            }
        }
        return true;
    }

ここでは, ブラウザから送出された HTTP_REFERER と SITE_URL 又は SSL_URL のドメインを比較し, 一致しない場合はエラー画面を表示させます.

つまり, 2.4.4 未満のバージョンでも, ブラウザの HTTP_REFERER 送出を無効にしない限りは, 攻撃が成立しません.

ほとんどのブラウザは, デフォルトで HTTP_REFERER を送出するので, あまり心配しなくても良いかもしれません.
ただし, セキュリティソフトにより HTTP_REFERER の送出が無効になる場合もありますので, 注意が必要です.

自動型変換の罠

PHP で, 改行区切りの数値を処理しようとして, めっちゃはまったのでメモ.

下記のようなテキストファイルを読み込み, 最小値から最大値までをループといった処理をする場合,

test.txt

1
3
2
0

こんな感じになると思います.

<?php
$contents = file('./test.txt');
sort($contents, SORT_NUMERIC);
$min = min($contents);
$max = max($contents);

for ($i = $min; $i < $max; $i++) {
    // some processing...
}
?>

このように, テキストファイルを読み込み, 配列に代入した段階では, まだ改行コードが残っています.
しかし, PHP の賢い(?)自動型変換機能は, 改行コードが含まれていようと, 数値とみなしてソートまでしてくれます.

<?php
$contents = file('./test.txt');
sort($contents, SORT_NUMERIC);
var_dump($contents);
array(4) {
  [0]=>
  string(2) "0
"
  [1]=>
  string(2) "1
"
  [2]=>
  string(2) "2
"
  [3]=>
  string(2) "3
"
}
?>

もちろん, 改行コードが含まれていようが, 最小値, 最大値も求めてくれます.

<?php
$min = min($contents);
$max = max($contents);
var_dump($min);
var_dump($max);
string(2) "0
"
string(2) "3
"
?>

比較演算子も完璧です.

<?php
var_dump($min < $max);
bool(true)
?>

しかし, 大きな罠が...

<?php
for ($i = $min; $i < $max; $i++) {
    // some processing...
}
?>

上記のように, for でループさせようとした場合, $i < $max で, ループの条件を評価するまでは良いんです.

<?php
$min = min($contents);
var_dump($min);
string(2) "0
"
$min++;
var_dump($min);
string(2) "0
"
?>

このように, 改行コードが含まれている場合, インクリメント演算子(++)で, インクリメントできません...

結果, 冒頭のプログラムは無限ループに陥ります(失笑)

ちなみに, 下記のように int にキャストするか trim してやれば問題ないです.

<?php
$min = (int) min($contents);
$min++;
var_dump($min);
int(1)
?>
<?php
$min = trim(min($contents));
$min++;
var_dump($min);
int(1)
?>

EC-CUBE を Solaris10 + Apache2.2 MPM worker + FastCGI で動作させる

EC-CUBESolaris 10 + Apache 2.2 MPM worker + FastCGI という, エンタープライズな環境で動かしてみるテスト.
データベースは, Solaris 10 にバンドルしている PostgreSQL 8.3 を使用しました.

FastCGI モジュールは, mod_fcgid を使用しています.

準備

予め, Solaris Software Companion CD から curl をインストールしておく.

# Solaris Software Companion CD をマウントしておく
$ cd /cdrom/s10_509_software_companion/Solaris_Software_Companion/Solaris_i386/Packages/
# pkgadd -d `pwd` SFWcurl

また, バンドルしている libiconv では, PHP5 のコンパイルが通らないので, sunfreeware.com から libiconv-1.11 パッケージをインストールしておく

$ curl -O ftp://ftp.sunfreeware.com/pub/freeware/intel/10/libiconv-1.11-sol10-x86-local.gz
$ gzip -d libiconv-1.11-sol10-x86-local.gz
$ su
# pkgadd -d libiconv-1.11-sol10-x86-local

Apache 2.2

$ curl -O http://ftp.riken.jp/net/apache/httpd/httpd-2.2.14.tar.gz
$ gzip -dc httpd-2.2.14.tar.gz | tar xvf -
$ cd httpd-2.2.14
$ ./configure --prefix=/opt/apache22 --with-ssl=/opt/sfw --enable-mods-shared=all --with-mpm=worker
$ make
$ su
# /usr/ccs/bin/make install

mod_fcgid

$ curl -O http://ftp.riken.jp/net/apache/httpd/mod_fcgid/mod_fcgid-2.3.4.tar.gz
$ gzip -dc mod_fcgid-2.3.4.tar.gz | tar xvf -
$ cd mod_fcgid-2.3.4
$ APXS=/opt/apache22/bin/apxs ./configure.apxs
$ make
$ su
# /usr/ccs/bin/make install

PHP5

PHP5.3.0 からは, --enable-fastcgi は常に有効となっている

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
$ curl -O http://jp.php.net/distributions/php-5.3.0.tar.gz
$ gzip -dc php-5.3.0.tar.gz | tar xvf -
$ cd php-5.3.0
$ ./configure --prefix=/opt/php5 --enable-zend-multibyte --with-gd --enable-gd-native-ttf --enable-gd-jis-conv --with-jpeg-dir=/usr --with-png-dir=/usr --with-freetype-dir=/usr/sfw --with-pgsql=/usr/postgres/8.3 --with-pdo-pgsql=/usr/postgres/8.3 --enable-mbstring --with-zlib-dir=/usr --with-pear
$ make
$ su
# LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/sfw/lib:/usr/sfw/lib:/opt/apache22/lib:/usr/local/lib make install

PostgreSQL

Solaris 10 には, PostgreSQL 8.3 がバンドルしているので, これを使う

$ su postgres
$ /usr/postgres/8.3/bin/init-db --no-locale -E UTF8 -D /var/lib/pgsql/data
$ /usr/postgres/8.3/bin/pg_ctl -w -D /var/lib/pgsql/data start
$ /usr/postgres/8.3/bin/createuser eccube_db_user
$ /usr/postgres/8.3/bin/createdb eccube_db

httpd.conf の設定

ソケット用のディレクトリを作成し, httpd ユーザーで書き込み可能にしておく

# mkdir -p /var/lib/fcgid/sock
# chown -R apache:apache /var/lib/fcgid

以下の内容で, /opt/php5/bin/php-wrapper を作成し, 実行パーミッションを付与する

#!/bin/sh
# Set desired PHP_FCGI_* environment variables.
# Example:
# PHP FastCGI processes exit after 500 requests by default.
PHP_FCGI_MAX_REQUESTS=10000
export PHP_FCGI_MAX_REQUESTS

# Replace with the path to your FastCGI-enabled PHP executable
exec /opt/php5/bin/php-cgi
# chmod +x /opt/php5/bin/php-wrapper

VirtualHost の定義はこんな感じ.

NameVirtualHost 192.168.1.2:80


  AddHandler fcgid-script  .php .cgi # 必ず .cgi をつけないとダメらしい
  FcgidIPCDir /var/lib/fcgid/sock
  IPCConnectTimeout 10
  IPCCommTimeout 20
  OutputBufferSize 0
  MaxRequestsPerProcess 500



    ServerAdmin nanasess@fsm.ne.jp
    DocumentRoot "/home/nanasess/workspace/eccube-branches/version-2_4/html"
    ServerName solaris.ec-cube.net
    ErrorLog "logs/solaris.ec-cube.net-error_log"
    CustomLog "logs/solaris.ec-cube.net-access_log" common
    DirectoryIndex index.php
    
    
        AllowOverride All
        Options +ExecCGI
        FCGIWrapper /opt/php5/bin/php-wrapper .php
        
            Order allow,deny
            Allow from all
        
    

EC-CUBE の設定

あとは, EC-CUBE をインストールするだけなのですが, .htaccessphp_flag や php_value のオプションが使用できないので, .htaccess のものはコメントアウトし, php.ini や httpd.conf で設定します.

パフォーマンスとか

今回, VMWare Fusion 上の Solaris 10 にインストールしたので, 負荷テストなど未実施です.
設定を詰めれば, おそらくモジュール版の PHP より良い性能を出してくれるはず...

EC-CUBE は, Linux のホストで稼動しているものがほとんどだと思いますが, 今では Solaris 10 も無償利用可能ですし, サポートを付けても, ベーシック・プランなら RHEL の半額程度です. OpenSolaris も気軽に利用できます.
レンタルサーバーでは難しいかもしれませんが, 専用サーバーの場合は Solaris 10 や OpenSolaris を検討しても良いかも.

オープンソースカンファレンス(OSC) 2009 KANSAI

本日, 京都で開催された, OSC2009-KANSAI でしゃべってきました.

オープンソースカンファレンス2009 Kansai - おいでやす

場所は京都コンピュータ学院.

京都コンピュータ学院 | 日本最初のコンピュータ教育機関

お題は, EC-CUBE の設計思想について.

めっちゃ濃い内容なので, あまり人が集まらないだろうなぁ... という予想に反して, 満員御礼の大盛況!

講義の内容は, twitter でも実況されました. (thanx AMUAMU さん)

EC-CUBE (@EC_CUBE) | Twitter

また, 資料を EC-CUBE Trac にアップしておきました.

OSC-KANSAI2009.ppt on WikiStart – Attachment – EC-CUBE Trac

いろいろな言語での urlencode

Web アプリケーションを運用していると, GET パラメータから値を取得しなければならないことが多々あると思います.

URL エンコードの方法は, RFC 2396 - Uniform Resource Identifiers (URI): Generic Syntax(旧 RFC1738) で定められており, 基本的には, この定義に基づいてエンコードしなくてはなりません.

PHP には, この RFC2396 に基いて URL エンコード/デコードを行う rawurlencode/rawurldecode 関数があります.

PHP: rawurlencode - Manual / PHP: rawurldecode - Manual

-_. を除くすべての非アルファベット文字をパーセント 記号 (%)に続いて 2 つの 16 進数がある表現形式に 置き換えた文字列を返します。これは、文字定数が特殊な URL デリミタと して解釈されたり、URL デリミタが(いくつかの電子メールシステムのような) 転送メディアにより文字変換されて失われてしまったりすることが ないように、RFC 1738 で定められたエンコーディング方法です。

HTML 4.01 Specification では, 「application/x-www-form-urlencoded MIME 形式でエンコードするように」とあります.

HTML フォームでデータの受け渡しをする場合は, こちらの方法を用いなくてはなりません.

RFC2396 では, 「スペース(空白文字)は,『%20』にエンコードしなさい」とあるのですが, HTML(RFC1866)では, 「スペースは『+』に置き換えなさい」とあります.

プログラム言語における, urlencode/urldecode は, この解釈がまちまちで, PHP の場合, HTMLにて規定された application/x-www-form-urlencoded MIME 形式に準拠した urlencode/urldecode 関数があります.

PHP: urlencode - Manual / PHP: urldecode - Manual

-_. を除くすべての非英数文字が % 記号 (%)に続く二桁の数字で置き換えられ、 空白は + 記号(+)にエンコードされます。 同様の方法で、WWW のフォームからポストされたデータはエンコードされ、 application/x-www-form-urlencoded メディア型も同様です。歴史的な理由により、この関数は » RFC 1738 エンコード( rawurlencode() を参照してください) とは異なり、 空白を + 記号にエンコードします。

しかし, 残念なことに, ブラウザによって "%XX" の形式に変換する対象の文字はまちまちです.

Java の URLEncoder#encode(String, String) のソースコードには, 下記のようなコメントがあります.

        /* The list of characters that are not encoded has been
         * determined as follows:
         *
         * RFC 2396 states:
         * -----
         * Data characters that are allowed in a URI but do not have a
         * reserved purpose are called unreserved.  These include upper
         * and lower case letters, decimal digits, and a limited set of
         * punctuation marks and symbols.
         *
         * unreserved  = alphanum | mark
         *
         * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
         *
         * Unreserved characters can be escaped without changing the
         * semantics of the URI, but this should not be done unless the
         * URI is being used in a context that does not allow the
         * unescaped character to appear.
         * -----
         *
         * It appears that both Netscape and Internet Explorer escape
         * all special characters from this list with the exception
         * of "-", "_", ".", "*". While it is not clear why they are
         * escaping the other characters, perhaps it is safest to
         * assume that there might be contexts in which the others
         * are unsafe if not escaped. Therefore, we will use the same
         * list. It is also noteworthy that this is consistent with
         * O'Reilly's "HTML: The Definitive Guide" (page 164).
         *
         * As a last note, Intenet Explorer does not encode the "@"
         * character which is clearly not unreserved according to the
         * RFC. We are being consistent with the RFC in this matter,
         * as is Netscape.
         *
         */

これによると, Netscape が "%XX" の形式に変換対象としない文字は,

"-", "_", ".", "*"

であり, 一方 Internet Explorer の場合は, 上記に加えて RFC で予約文字と規定している「@(アットマーク」も変換対象としません.

"-", "_", ".", "*", "@"

安全にデータを受け渡すためには, ブラウザの仕様に準拠した方が安全... ということで,
O'Reilly の "HTML: The Definitive Guide" の 164 ページに書いてある内容に従うことにしたようです.

そんなわけで, PHP の urlencode/urldecode と, Java の URLEncoder#encode(String, String)/URLDecoder#decode(String, String) は, 「*(アスタリスク)」の解釈が異なるため, 注意が必要です.

世の中では, PHP で書かれた Webアプリケーションが多いと思われますし, 他の言語で URLエンコードした内容を PHP で urldecode というケースは多々あると思います.

これを言語別に見てみましょう.

Java の場合

Java の場合, 前出のように「*(アスタリスク)」を変換対象としません.

PHP の urldecode は, urlencode 関数の変換対象文字が変換されずに入ってきたとしても, 何もせずに返すので, 問題は発生しないでしょう.

安心して URLEncoder#encode(String, String) を使用できます. 文字エンコーディングは, UTF-8 の使用が推奨されています.

http://sdc.sun.co.jp/java/docs/j2se/1.4/ja/docs/ja/api/java/net/URLEncoder.html

C# の場合

C# の場合は, 一般的に HttpServerUtility.UrlEncode(String) メソッドを使用します.
このメソッドの英数字以外の非変換対象文字は下記のようです.

"'", ".", "-", "*", ")", "(", "_"

これだけ見ると, 安心して使えそうなのですが, 実際に実行してみると, 16進数に変換すべき部分が小文字になってしまいます.

これでは PHP の urldecode で処理できませんので, 自前でエンコードしてやる必要があります.

private String urlEncode(String argment) 
{ 
    foreach (Match m in (new Regex("[^a-zA-Z0-9._-]")).Matches(argment)) 
        argment = argment.Replace(m.Value, "%" + BitConverter.GetBytes(m.Value[0])[0].ToString("X")); 
    return argment.Replace("%20", "+"); 
} 

HttpServerUtility.UrlEncode Method (System.Web) | Microsoft Docs

Perl の場合

URI::Escape という関数がありますが, RFC2396 に厳密に準拠しており, 空白文字を「+」に変換してくれません...

http://search.cpan.org/dist/URI/URI/Escape.pm

PHPJava, C# のように, 空白文字を「+」へ置換するには自前で処理を書かなくてはなりません.

下記のような正規表現PHP の urlencode と同じ振舞いになります.

$str =~ s/([^a-zA-Z0-9-_. ])/'%' . uc(unpack('H2', $1))/eg; 
$str =~ tr/ /+/; 

おわりに

たまたま, いろいろな言語で URL エンコードしたものを PHP で urldecode しなければならない案件があり, いろいろ調べたみたのですが, 言語により振舞いがまちまちで, とっても面倒です...

本来, 最終的には, ブラウザが encode/decode する部分なので, ブラウザの振舞いにできるだけ近づけた(しかもコメントに理由がしっかり書いてある) Java の実装が素直で良い気がします.

ハングリーアート

漫画はハングリーアートだよ
何かに追い詰められて飢えてないと描けない
そんな事で何回もジレンマに陥ったことがある
それを乗り越えられたのは自分がやらねば誰がやるのだと
ただがむしゃらに仕事をしたからではないだろうか

手塚治虫

そう. 自分がやらねば誰がやるのだ.

最近は, バーンアウト気味なのが続いて, がむしゃらに仕事できてなかったなぁ...

追記 - svk sync でエラー

下記のようなログが見つかったので, 修正してみたけれど, 変化無し.
http://lists.bestpractical.com/pipermail/svk-users/2008-May/000246.html

よく調べてみると, 該当のリビジョンで同期しようとしたディレクトリ以下のディレクトリが削除されてる...

どうやら, これが原因でうまく動作しないらしい.

仕方がないので,

$ svk sync -s 17367 //foo

として, 該当のリビジョンを飛ばして同期してみたら, できたっぽい.

svkリポジトリを見ても, 修正された様子が無いし...
http://svn.bestpractical.com/cgi-bin/index.cgi/svk/

Perl は得意じゃないので, サクっと直すわけにもいかないし, とりあえずこのままでorz