セッション管理の不備、つまりセッション・ハイジャック、セッション・フィクセーションの脆弱性と、クロスサイト・リクエスト・フォージェリ(CSRF)の脆弱性について、EC-CUBE 2.12.6 を対象に見てみました。
結論をまず申し上げますと、「対策はなされていますけれども、わたくしのレベルが低くて充分なセキュリティ強度があるかどうかは結局わからなかった」です。
ウェブ健康診断仕様の 18 ページ、 (K) セッション管理の不備の検出パターン、「1 ログインの前後でセッションID が変化するか」で「セッションID が変わらない場合」に脆弱性ありとしています。
EC-CUBE 2.12.6 を調べてみましたら、会員ログイン時も、店舗管理者ログイン時も、ログイン前後でセッション ID は不変でございました。脆弱性がありますわ!まあ!どういたしましょう!
そう思いつつ、安全なウェブサイトの作り方の対応セクション、16 ページからの「1.4 セッション管理の不備」を拝見いたしますと、■ 根本的解決の部分に次のようにありました。
- ■ 根本的解決「4-(iv)-a ログイン成功後に、新しくセッションを開始する。」
あら、やっぱり EC-CUBE 2.12.6 には脆弱性があるのでしょうか?もう少し読み込んでみますと、次の対策でも良いとのこと。
- ■ 根本的解決「4-(iv)-b ログイン成功後に、既存のセッションIDとは別に秘密情報を発行し、ページの遷移ごとにその値を確認する。」
また、同様のことが書籍『体系的に学ぶ安全なWebアプリケーションの作り方』にも記載されております。
- P181 に、認証後に「セッションIDの変更ができない場合はトークンにより対策する」ことで、「トークンによりセッションIDの固定化攻撃を防御できます」とのこと。
この対策は、EC-CUBE 2.12.6 のログイン機能に施されているでしょうか?調べましたので記録を残しておきます。
なお、セッション管理の不備について調べていくうちに、トークンをページ遷移ごとに確認する方法はクロスサイト・リクエスト・フォージェリ対策にもなることに気が付きましたのでまとめてノートです♪
★EC-CUBE がセッション・ハイジャック、セッション・フィクセーション、CSRF に対向するために行っているページ遷移ごとのトークン確認内容
POST の場合に、リクエストしたページの hidden に埋め込まれた「トランザクションID」とセッションに格納済みの「トランザクションID」とが一致するかを確認することで対策を行っているようですの。
ただし、気になる違いがございます。
★トークン確認による解決方法に対する安全なウェブサイトの作り方、体系的に学ぶ安全なWebアプリケーションの作り方と EC-CUBE の仕様との乖離点 2 つ
2つの参考書と EC-CUBE では次の違いがありました。
- 参考書では秘密情報を発行するタイミングがログイン成功後であるのに対し、EC-CUBE ではサイトを訪問した時の最初のページアクセス時にトークンを生成
- 参考書では秘密情報(トークン)の保存場所が Cookie なのに対し、EC-CUBE では HTML の hidden に埋め込み
この 2 点のせいで、やっぱり脆弱性ありとなってしまわないでしょうか?わかりません><。
参考書にありました、ログイン成功後にトークンを生成というタイミングの問題に関して考えます。
サイトへの最初のアクセス時よりもログイン後の生成の方がトークンの生存期間が長いといえます。また、ログイン後に生成であれば攻撃者はログイン ID やパスワードを知らないためトークンを知る手段がないように思えます。よって、ログイン後の生成の方がリスクが低い、、、と考えましたけれどもどうでしょうか。。。トークンが盗まれない場所にあればいつ生成しても同じだよ♪との反論が成立するかしら。。。わかりません><。
hidden にトークンが埋め込む場合について考えますとどうでしょうか。
HTTPS で 通信している場合や Cookie のセキュア属性が有効な場合はどちらでも暗号化されますので両方共盗聴された後の強度は同じと言えると思いますの。では、SSL 暗号化がされていない場合は?どうでしょうか?どちらがより盗みやすい、盗みにくいによりセキュリティ強度が変わってくると言えます。
クッキーの盗聴やトークンによる対策については、『体系的に学ぶ安全なWebアプリケーションの作り方』にひと通りの対策がございますので何度も読み返したく存じます。該当箇所は次です。
- 「4.6.4 セッションIDの固定化」について説明されている中で、P 181 「対策」の「◆ セッションIDの変更ができない場合はトークンにより対策する」
- 「4.8.2 クッキーのセキュア属性不備」の中で、P 214 「◆ クッキーにセキュア属性がつけられないアプリケーションとは」や、P215 「◆ トークンを用いた対策」
クッキーは盗聴される可能性があるとのことですね。また hidden の設定項目は POST 時や HTTP のレスポンスで漏れたりするのでしょうか。。。わかりません><
hidden については、先の書籍に次のような記述がございました。
- P38 ◆ hiddenパラメータのメリット として「hiddenは利用者自身からは書き換 えできるものの、情報漏洩や第三者からの書き換えに対しては堅牢」とある。
ということは、クッキーと同程度かそれ以上、漏れにくいものとの感覚でよいのでしょうか。。。
また、EC-CUBE 自体の次の仕様によって、セキュリティ強度が上がっていると判断できます。
- EC-CUBE では、トークンの妥当性チェックが NG となった場合、トークンは破棄される。これにより、利用者自身によりトークン書き換えが発生した場合、エラーページを表示することで操作を中断することができる。
結局、EC-CUBE のやり方で問題ないのかどうか、結論付けられません!よいような気もいたしますけれども。。。参考書籍と EC-CUBE の仕様との違いはたったの 2 箇所ですけれども、わからない〜!わからな〜い♪ことばかりですの><。
★EC-CUBE のページの遷移ごとにトークンの値を確認する仕組みをソースを読んで理解します
ここからは気を取り直して、実際にソースを追って、どのようなロジックなのか確かめますわ。
ジャンプ元ページと、ジャンプ先ページの HTML で次のようなコードを出力させますの。
<input type='hidden' name='transactionid' value="SC_Helper_Session::getToken 関数の返り値" />
このためには、テンプレートでトランザクションID を出力させます。
ですけれども、デフォルトのテンプレートを使用している場合は、予め次のように PC (default)、携帯(mobile)、スマートフォン(sphone)に必要なページ、ブロックに埋め込まれているので、たいていは必要ないかと存じます。
<input type="hidden" name="<!--{$smarty.const.TRANSACTION_ID_NAME}-->" value="<!--{$transactionid}-->" />
このようにしますと、次に説明しますロジックでチェックがなされるようです。
■漏らさずチェックするためにすべてのページ表示時に動く init() でページアクセス時にトークンの値を確認しています!
ページを表示するときには必ず実行される LC_Page::init 関数の最後あたりでトークンつまりトランザクションID の妥当性の確認と、その次にすかさずトークンの生成を行っています。
次に解説をいたしますが、トークンの検証は POST アクセス時のみですし、トークンの作成は、サイト訪問初回時のみです。
data/class/pages/LC_Page.php
/**
* Page を初期化する.
*
* @return void
*/
function init() {
// 開始時刻を設定する。
$this->timeStart = microtime(true);
$this->tpl_authority = $_SESSION['authority'];
// ディスプレイクラス生成
$this->objDisplay = new SC_Display_Ex();
$layout = new SC_Helper_PageLayout_Ex();
$layout->sfGetPageLayout($this, false, $_SERVER['SCRIPT_NAME'],
$this->objDisplay->detectDevice());
// スーパーフックポイントを実行.
$objPlugin = SC_Helper_Plugin_Ex::getSingletonInstance($this->plugin_activate_flg);
$objPlugin->doAction('LC_Page_preProcess', array($this));
// 店舗基本情報取得
$this->arrSiteInfo = SC_Helper_DB_Ex::sfGetBasisData();
// トランザクショントークンの検証と生成
$this->doValidToken();
$this->setTokenTo();
// ローカルフックポイントを実行.
$this->doLocalHookpointBefore($objPlugin);
}
■トークンの検証関数 doValidToken()、トークン生成関数 setTokenTo()
では次に、doValidToken 関数と、setTokenTo 関数の中身を見てみましょうね。
doValidToken 関数で hidden で渡されてきたトランザクションID の値が妥当かどうかチェックをいたします。
- doValidToken 関数がチェックを行うのは POST の場合のみ
というところがこの関数のポイントと存じます。
- GET のときは、たとえば商品一覧検索結果へ外部サイトからリンクを貼られることを想定しますのでチェックしない
- 商品購入でセッションの値を渡しているときは結局 POST いたしますのでチェックする
- POST の時はチェックする
と考えることができますので、POST の時のみチェックしているのだと存じます。勉強になりますこと♪
/**
* POST アクセスの妥当性を検証する.
*
* 生成されたトランザクショントークンの妥当性を検証し,
* 不正な場合はエラー画面へ遷移する.
*
* この関数は, 基本的に init() 関数で呼び出され, POST アクセスの場合は自動的に
* トランザクショントークンを検証する.
* ページによって検証タイミングなどを制御する必要がある場合は, この関数を
* オーバーライドし, 個別に設定を行うこと.
*
* @access protected
* @param boolean $is_admin 管理画面でエラー表示をする場合 true
* @return void
*/
function doValidToken($is_admin = false) {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (!SC_Helper_Session_Ex::isValidToken(false)) {
if ($is_admin) {
SC_Utils_Ex::sfDispError(INVALID_MOVE_ERRORR);
} else {
SC_Utils_Ex::sfDispSiteError(PAGE_ERROR, '', true);
}
SC_Response_Ex::actionExit();
}
}
}
/**
* トランザクショントークンを取得し, 設定する.
*
* @access protected
* @return void
*/
function setTokenTo() {
$this->transactionid = SC_Helper_Session_Ex::getToken();
}
■トークンを確認している具体的な箇所である SC_Helper_Session_Ex::isValidToken とトークンを発行する具体的な箇所である SC_Helper_Session_Ex::getToken
先ほどの doValidToken 関数から SC_Helper_Session_Ex::isValidToken 関数を、そして setTokenTo 関数から SC_Helper_Session_Ex::getToken 関数を呼んでおります。今度はこれを見てみましょうね♪
なお、getToken 時に使用される createToken 関数も記載しておりますが、これはどのように乱数を生成しているか見たかったためですわ。
ポイント言う程でもないかもしれませんが、気になったのは getToken 関数でセッションにトランザクション ID が格納されているかどうかを確認している点ですの。
- EC-CUBE 2.12.6 ではセッションが生きている限り、同じトランザクションID が使用される。
ということが言えると思います。ここで empty 関数による判定を行わなければ、
- ジャンプ元のページでトランザクション ID を生成し、
- ジャンプ先で確認し合格。
- その後、今までのトランザクション ID を破棄して新しいトランザクション ID をジャンプ先ページに出力
ということになると思うのです。この方が、よりトランザクション ID が攻撃者に類推されにくくなるように思ったのですけれども、考えすぎなのでしょうか。
勉強不足でなんの根拠もございませんが、おそらく考え過ぎで、トランザクション ID の発行し直しは意味のない行為で、このままでよい、なのだと思います。
data/class/helper/SC_Helper_Session.php
/**
* トランザクショントークンを生成し, 取得する.
*
* 悪意のある不正な画面遷移を防止するため, 予測困難な文字列を生成して返す.
* 同時に, この文字列をセッションに保存する.
*
* この関数を使用するためには, 生成した文字列を次画面へ渡すパラメーターとして
* 出力する必要がある.
*
* 例)
* <input type='hidden' name='transactionid' value="この関数の返り値" />
*
* 遷移先のページで, LC_Page::isValidToken() の返り値をチェックすることにより,
* 画面遷移の妥当性が確認できる.
*
* @access protected
* @return string トランザクショントークンの文字列
*/
function getToken() {
if (empty($_SESSION[TRANSACTION_ID_NAME])) {
$_SESSION[TRANSACTION_ID_NAME] = SC_Helper_Session_Ex::createToken();
}
return $_SESSION[TRANSACTION_ID_NAME];
}
/**
* トランザクショントークン用の予測困難な文字列を生成して返す.
*
* @access private
* @return string トランザクショントークン用の文字列
*/
function createToken() {
return sha1(uniqid(rand(), true));
}
/**
* トランザクショントークンの妥当性をチェックする.
*
* 生成されたトランザクショントークンの妥当性をチェックする.
* この関数を使用するためには, 前画面のページクラスで LC_Page::getToken()
* を呼んでおく必要がある.
*
* トランザクショントークンは, SC_Helper_Session::getToken() が呼ばれた際に
* 生成される.
* 引数 $is_unset が false の場合は, トークンの妥当性検証が不正な場合か,
* セッションが破棄されるまで, トークンを保持する.
* 引数 $is_unset が true の場合は, 妥当性検証後に破棄される.
*
* @access protected
* @param boolean $is_unset 妥当性検証後, トークンを unset する場合 true;
* デフォルト値は false
* @return boolean トランザクショントークンが有効な場合 true
*/
function isValidToken($is_unset = false) {
// token の妥当性チェック
$ret = $_REQUEST[TRANSACTION_ID_NAME] === $_SESSION[TRANSACTION_ID_NAME];
if ($is_unset || $ret === false) {
SC_Helper_Session_Ex::destroyToken();
}
return $ret;
}
おわりに
中途半端で終わってしまいました><。わたくしたち、まだまだ未熟です><。
ですけれども、本投稿で大活躍いたしました書物をあらためて紹介いたします。
- IPA 独立行政法人 情報処理推進機構:安全なウェブサイトの作り方
- 体系的に学ぶ 安全なWebアプリケーションの作り方(ソフトバンク クリエイティブ)(著:徳丸 浩) – 電子書籍ファンのための出版社直営電子書籍モール「ブックパブ」
また、次のウェブページもセッション管理の不備や CSRF について参考になりました。ありがとう存じます。
- 「セッション管理」のすべて – ステップ2 [セキュリティ対策] セッションIDを暗号化,URL埋め込みは危険:ITpro
- Webアプリケーションに潜むセキュリティホール(3):気を付けたい貧弱なセッション管理 – @IT
以上です。
