2013年2月23日土曜日

自己流のSQLインジェクション対策は危険

SQLインジェクションについて解説したブログ記事を読んだところ、ユニークな対策方法が紹介されていました。この対策方法の問題点について解説し、正しい脆弱性対処の重要性を説明します。

ユニークなSQLインジェクション対策


SQLインジェクションのブログ記事を読んでいて、対策として以下のスクリプトが紹介されていました。
$_GET[id]=strip_tags(htmlspecialchars($_GET[id]));
$_GET[id]=preg_replace(array(‘/[~;\'\"]/’,'/–/’),”,$_GET[id]);
現在は当該のスクリプトは削除されているので、当該エントリは参照せず、「昔々あるところにこのようなSQLインジェクション脆弱性対処スクリプトがあった」という想定で以下の議論を進めます。

問題点の説明

このスクリプトはいくつかの問題点があります。
  • $_GETを直接変更していて汎用性が乏しい
  • idをクォートしていないなどいくつかの文法エラー
  • 表示前提でないのにHTMLエスケープしている
  • HTMLエスケープした後strip_tagsを呼んでいるが、この段階ではタグはエスケープ済みなので、strip_tagsは何もしない
  • 「危険な文字」を削除するサニタイジング手法を用いている
このままでは動かすことができないので、以下のようにsanitize関数として少し修正したものを紹介します。
function sanitize($str) {
  $str = strip_tags(htmlspecialchars($str));
  return preg_replace(array('/[~;\'\"]/', '/-/'), '', $str);
}
この関数に、さまざまな文字を通した結果を下表に紹介します。

入力文字htmlspecialcharsstrip_tagspreg_replace(最終)
AAAA
5555
<&lt;&lt;&lt
>&gt;&gt;&gt
&&amp;&amp;&amp
"&quot;&quot;&quot
'''(空)
\\\\
;;;(空)
---(空)
~~~(空)
####

HTMLエスケープの対象となる < > & " の4文字は、文字実体参照に変換された後、preg_replace関数でセミコロンを削除してしまうので、中途半端な妙な文字化けになりそうです。
一般的な原則としては、データベースにはHTMLの形ではなくプレーンテキストの形で保存しておき、HTMLとして表示する直前にHTMLエスケープする方法で統一することで、上記のような文字化けやエスケープ漏れをなくすことがよいでしょう。

脆弱性はないのか

このsanitize関数に脆弱性はないでしょうか。上表のように、バックスラッシュ(円記号)を素通ししているので、MySQLや、設定によってはPostgreSQLの場合に、問題が生じそうです。以下、それを説明します。以下の説明では、MySQLを使う想定とします。
以下のように、ログイン処理を想定したSQL文組立があったとします。
$sql = sprintf("SELECT * FROM users WHERE id='%s' AND pwd='%s'", sanitize($id), sanitize($pwd));
ここで、$idと$pwdが以下だったとします。
$id = '\\';   // \ 1文字
$pwd = 'password';
$sqlは下記となります。
SELECT * FROM users WHERE id='\' AND pwd='password'
ここで、id=の後の文字列リテラルはどこまででしょうか。\'は「シングルクォートを\でエスケープしたもの」、すなわちリテラル内の文字と解釈されるので、その次のシングルクォートまでが文字列リテラルになります。つまり、 AND pwd=まで(黄色でマークした部分)が文字列リテラルと解釈されます。その後の password' は「文字列リテラルの外」、すなわちSQL文の構文と解釈されますが、SQL文として正しくないので、このSQL文は構文エラーになります。
それでは、passwordのところをSQL文として正しくなるように変更したらどうでしょうか。これがSQLインジェクション攻撃です。その例を以下に示します。
$id = '\\';   // \ 1文字
$pwd = ' OR 1=1#';
組み立てられたSQL文は以下となります。
SELECT * FROM users WHERE id='\' AND pwd=' OR 1=1#'
passwordの代わりに、OR 1=1(青くマークした部分)が正しいSQL文(の一部)となります。#以降はMySQLのコメントとして無視されます。すなわち、これで認証回避が可能となりました。

「自己流の脆弱性対策」の危険性

以上に説明したように、htmlspecialchars、strip_tags、preg_replaceの三種類の関数を使った「独自の」でSQLインジェクション対策には抜けがあり、SQLインジェクション攻撃を許してしまう可能性があることがわかりました。
SQLインジェクションを含めて、脆弱性対処の方法論は、世界中の専門家がさまざまな観点から研究しています。SQLインジェクション対策に関しては、数年前から「究極形」がわかっており、特別な事情のある場合を除いて、わざわざ別の方法を用いる必要はないでしょう。特別な場合とは以下の様なケースです。
  • データベースアクセスを含むフレームワークを新たに開発する場合
  • O/Rマッパーを自作する場合
  • phpMyAdminのようなデータベース管理ツールを自作する場合
大半の開発者は、上記を作らないでしょう。だから、以下に示す、SQLインジェクション対策の「究極形」に従うことを強く推奨します。それは下記のとおりです。
  • 文字列連結(sprintf等を含む)を用いてSQL文を組み立てない
  • 静的プレースホルダを用いてSQL文にパラメータを割り当てる
  • データベース接続時に文字エンコーディングを指定する
上記の理論的な根拠については、IPAの「安全なSQLの呼び出し方」を参照下さい。また、PHP+PDOを用いた実際的なSQLアクセスの方法については、「PDOにおける一応の安全宣言と残る問題点」を参照下さい。
また、SQLインジェクション対策の解説をこれから書こうとする人には、ぜひajiyoshiさんの素晴らしいブログ記事「Webアプリケーションとかの入門本みたいのを書く人への心からのお願い。」をお読み下さい。このような文章が、現場の開発者から出てくることは本当に素晴らしいことです。

まとめ

自己流の脆弱性対処の危険性について、具体例を用いて説明しました。SQLインジェクションは、非常に危険な脆弱性ですが、完全な対処の仕方が分かっていて、正しいプログラミングをする限り恐れる必要はありません。ここで紹介した解説資料を参照して、あなたのWebサイトを安全にしてください。



2013年2月15日金曜日

ログアウト機能の目的と実現方法

このエントリでは、Webアプリケーションにおけるログアウト機能に関連して、その目的と実現方法について説明します。

議論の前提

このエントリでは、認証方式として、いわゆるフォーム認証を前提としています。フォーム認証は俗な言い方かもしれませんが、HTMLフォームでIDとパスワードの入力フォームを作成し、その入力値をアプリケーション側で検証する認証方式のことです。IDとパスワードの入力は最初の1回ですませたいため、通常はCookieを用いて認証状態を保持します。ログアウト機能とは、保持された認証状態を破棄して、認証していない状態に戻すことです。

Cookieを用いた認証状態保持

前述のように、認証状態の保持にはCookieを用いることが一般的ですが、Cookieに auth=1 とか、userid=tokumaru などのように、ログイン状態を「そのまま」Cookieに保持すると脆弱性になります。これについては、以前のエントリ「CookieにログインIDを保存してはいけない」にて説明しました。
このため、通常はセッション変数にログイン状態を記録し、CookieにはセッションIDのみを保存します。セッション変数であれば、外部から書き換えることは不可能なので、Cookieに生の値を入れる方法に比べて安全です。

ログアウト機能は本当に必要なのか?

ここで、Webアプリケーションにおいて、本当にログアウト機能が必要なのかを検討します。
仮に、ログイン機能のあるWebアプリケーションにログアウト機能がない、あるいはログアウト機能が不完全である場合、脆弱性診断業者は、この状態を脆弱性と指摘しますが、通常は「低危険度の脆弱性」と判定すると思います。なぜ、低危険度なのでしょうか。

実は私達は、ログアウト機能のない認証方式を知っています。それはHTTP認証(BASIC認証やダイジェスト認証の総称)です。HTTP認証の場合、サーバー側で認証状態は保持せずにリクエスト毎に認証するので、そもそもログアウトという概念がない(認証状態を保存しないので破棄の必要がない)のですが、利用者が入力したIDとパスワードをブラウザが一時的に保存するため、外見上は「ログイン状態が保持されている」ように見えます。
HTTP認証はログアウトができないので不便だという意見はよく目にしますし、それを克服するため、401応答ヘッダを返すことでHTTP認証のログアウトを実現するという例も見かけます。しかし、一般的には、HTTP認証はログアウトができない(ブラウザがIDとパスワードを保持したままになる)ので、以下のような「運用で対処」している場合が多いでしょう。

  • ブラウザを終了する(HTTP認証のID・パスワードは消える)
  • ブラウザの機能によりHTTP認証のID・パスワードを消去する

ブラウザによる認証情報消去機能の例として、Firefoxの画面を以下に紹介します。以下は「最近の履歴を消去」メニューにより表示されるダイアログです。


ここで「現在のログイン情報」にチェックして「今すく消去」ボタンをクリックすると、HTTP認証のIDとパスワードが消去され、見かけ上はログアウトしたように見えます。
これと類似のことは、フォーム認証でもできます。セッションCookie(Expires指定のないCookie)はブラウザの終了と共に消えますし、ブラウザの機能によりCookieを削除することもできます。すると、利用者からは、認証していない状態、すなわちログアウトした状態に見えます。

以上のように、我々は「ログアウト機能のない世界」を知っており、一応それを受け入れていることになります。もしもHTTP認証にログアウト機能(アプリケーションからHTTP認証情報を破棄する機能)がないことが本質的に危険であれば、たとえばJavaScriptの機能としてHTTP認証情報を破棄するメソッドが実装されてもよさそうなものですが、そのような機能はありません。

HTTP認証とフォーム認証では、状態保持の場所が違う

ここで、HTTP認証とフォーム認証では、状態保持の場所が異なることを確認します。
HTTP認証は、認証状態を保持しておらず、リクエスト毎に認証処理が動きますが、利便性のため、一度入力したIDとパスワードはブラウザがキャッシュしています。
一方、フォーム認証の一般的な実装では、認証結果をセッション変数に記憶します。セッション変数はサーバー側で保存する場合が多いので、結果として、認証状態はサーバー側で保持していることになります。以上の内容を下表に整理します。

保持する内容 保存場所
BASIC認証 IDとパスワード ブラウザ
フォーム認証 認証結果 サーバー

すなわち、フォーム認証の場合、セッションIDのクッキーを消しただけでは、サーバー側に認証状態は残ったままということになります。

ログアウト機能がないと困ること

先に、HTTP認証ではアプリケーション側でのログアウト機能が一般的でなく、我々はそれを受け入れていると説明しましたが、フォーム認証も含め、ログアウト機能がないことで困ることを検討します。

まず問題になるのは、共有PCを使っている場合や、離席中のパソコンを勝手に使われるケースです。この場合、ログインしたままのアプリケーションがあれば、それを勝手に使われ、なりすましができてしまいます。

これに対しては、以下の回避策があります。
  • 共有PCでは認証機能のあるアプリケーションを使わない
  • 共有PC使用後はブラウザを閉じる
  • アプリケーションのセッションタイムアウト時間を短くする。セッションタイムアウトすると認証情報も破棄される
  • 離席中のPCを使われること自体が重大な問題なので、離席時にはPCをロックする
  • 同じく、離席中はノートPCを持ち歩く
次に検討するのは、クロスサイト・スクリプティング(XSS)やクロスサイト・リクエストフォージェリ(CSRF)の回避策としてのログアウト機能です。これら受動的攻撃は、ログアウト状態であれば、セッションハイジャックやCSRFによる重要な機能の悪用は避けることができます。

最後に、仮に通信路上でセッションIDが盗聴された場合でも、ログアウトしてしまえば、それ以降の悪用を避けることができます。セッションIDが盗聴されるシナリオでは、ログイン時のIDとパスワードも盗聴されるとも考えられますが、以下のようなサイトであれば、「IDとパスワードは盗聴されないが、セッションIDは盗聴される」という状況は起こりえます。
  • 認証フォームはSSLでIDとパスワードを暗号化して送信している
  • 認証後のページは平文のHTTPであり、セッションIDも平文送信される
このようなサイトの場合、利用者がログアウトすると、その後のセッションハイジャックは止めることができます。

ログアウト機能の効用のまとめ

以上に見てきたように、ログアウト機能は本質的に何かを解決する機能ではありません。何か悪いことが起こった際に、被害を軽減するものでしかありません。下表に、ログアウト機能による被害権限の内容と、対応する根本的解説策をまとめました。

ログアウトによる被害軽減 根本的解決策
離席中のなりすましの防止 離席中はPCをロックする、あるいはノートPCを持って移動する
共有PCによるなりすましの防止 共有PCではログインしない、あるいは利用後にブラウザを閉じる
XSS被害の軽減 XSS脆弱性をなくす
CSRF被害の軽減 CSRF脆弱性をなくす
セッションID盗聴の被害軽減 サイト全体をSSLで暗号化する

このため、HTTP認証でログアウト機能がないことも、「不便だが本質的ではないので許容しよう」ということになっているのでしょう。

ログアウト機能の実装法

ここからは、フォーム認証におけるログアウト機能の実装方法について説明します。
認証状態はセッション変数に保持されているので、以下のいずれかにより、確実に認証状態を破棄することができます。
  • セッション自体を破棄する
  • 認証状態を保持するセッション変数に「ログインしていない」ことを示す値を代入する
PHPの場合、セッションの破棄は以下で実現出来ます。
session_destroy();
しかし、このままだとセッション変数の値までは破棄されません。このため、ログアウト処理のページで、ログアウト機能以外にも実行する場合は、以下のようにセッション変数の破棄も行うと良いでしょう。
session_start();
$_SESSION = array();
session_destroy();
個人的には、ログアウトページでログアウト以外のことをするシナリオは想像がつきませんが、絶対にないとは言い切れないので、保険的にセッション変数の破棄もやっておくとよいということです。

また、ショッピングサイトなどで、ログアウトはするがセッションは破棄したくないという場合も有り得ます。その場合は、下記のように、認証状態のみをクリアします。
$_SESSION['userid'] = false;

セッションIDのCookieを破棄する必要はあるか

PHPのマニュアルのsession_destroy()関数の説明には、ログアウト時にはセッションIDのクッキーを削除しなければならない(原文は"the session cookie must be deleted")とあります。
セッション ID の受け渡しに クッキーが使用されている場合(デフォルト)には、セッションクッキーも 削除されなければなりません。
http://www.php.net/manual/ja/function.session-destroy.phpより引用
しかし、私は、セッションIDのクッキー(PHPのデフォルトはPHPSESSID)は削除する必要はないと考えます。その理由は、セッションが破棄された状態では、セッションIDは単なる乱数であり、攻撃の余地はないからです。

これはホテルのカードキーに例えることができます。最近のホテルはカードキーを採用するところが増えていますが、カードキーにも二種類あり、部屋ごとのカードを使いまわす場合と、チェックアウトのたびにカードキーをリセットして、別のカードキーを使う場合があります。
後者の場合、チェックアウト済みのカードキーには価値がないので、顧客が記念に持ち帰ってもよいところも多いですね。
セッション変数はホテルの部屋に例えることができます。部屋にアクセスするにはカードキー(セッションID)が必要です。顧客は滞在中カードキーさえあればいつでも部屋(セッション変数)にアクセスできますが、チェックアウト(ログアウト)すると、部屋にアクセスできなくなります。入室できないカードキーには意味が無いので、持ち帰っても差し支えないというわけです。

セッションIDには、記念品としての意味はありませんが、認証状態を破棄するという意味においては、ブラウザ側に残っていても不具合はありません。これが、「ログアウト時にセッションIDのCookieを破棄する必要はない」という根拠になります。

セッションIDのCookieを削除したい理由

とはいえ、セッションIDのCookieを削除してはいけない理由もないので、場合によっては削除してもよいでしょう。セッションIDのCookieを削除したくなる理由をいくつか考えてみました。他にもあればご指摘下さい。

  • 不要なものが出続けるのはなんとなく気持ち悪い
  • リクエストヘッダが不要なCookieのサイズ分大きくなるのは非効率である
  • プライバシーポリシーにおいて、Cookieはログイン中のみ使用すると約束している
  • ログアウト後もCookieが送信されることにより、トラッキング等の「あらぬ疑い」をかけられたくない

私は、セキュリティコンサルタントとしては珍しく(?)、余計なこと(保険的対策としても効果の薄いもの)をするのは嫌いなので、「Cookieが残るのは許容してもいい」という意見ですが、Cookieを削除すべきでないとまでは思いません。

脆弱性診断の実務として、ログアウト後にセッションIDのCookieが残ることを指摘する業者はあるかもしれませんが、指摘したとしても「情報」(脆弱性とまでは言えないが一応ご報告)というレベルだと思います。


まとめ

  • ログアウト機能の目的は、セッションハイジャックなどに対する保険的対策である
  • ログアウト機能の実現方法は、セッションを破棄するか、認証状態を保持するセッション変数をクリアすることである
  • ログアウト時にセッションIDのCookieを削除する必要はないが、別の理由があれば削除してもよい

フォロワー