セキュリティのベストプラクティス

下記において、一般的なセキュリティの指針を復習し、Yii を使ってアプリケーションを開発するときに脅威を回避する方法を説明します。

基本的な指針

どのようなアプリケーションが開発されているかに関わらず、セキュリティに関しては二つの大きな指針が存在します。

  1. 入力をフィルタする。
  2. 出力をエスケープする。

入力をフィルタする

入力をフィルタするとは、入力値は決して安全なものであると見なさず、取得した値が実際に許可さていれる値に含まれるか否かを常にチェックしなければならない、ということを意味します。 例えば、並べ替えが三つのフィールド titlecreated_at および status によって実行され、フィールドの名前がユーザの入力によって提供されるものであることを知っている場合、取得した値を受信するその場でチェックする方が良い、ということです。 基本的な PHP の形式では、次のようなコードになります。

$sortBy = $_GET['sort'];
if (!in_array($sortBy, ['title', 'created_at', 'status'])) {
    throw new Exception('sort の値が不正です。');
}

Yii においては、たいていの場合、同様のチェックを行うために フォームのバリデーション を使うことになるでしょう。

出力をエスケープする

データを使用するコンテキストに応じて、出力をエスケープしなければなりません。 つまり、HTML のコンテキストでは、<> などの特殊な文字をエスケープしなければなりません。 JavaScript や SQL のコンテキストでは、対象となる文字は別のセットになります。 全てを手動でエスケープするのは間違いを生じやすいことですから、Yii は異なるコンテキストに応じたエスケープを実行するためのさまざまなツールを提供しています。

SQL インジェクションを回避する

SQL インジェクションは、次のように、エスケープされていない文字列を連結してクエリテキストを構築する場合に発生します。

$username = $_GET['username'];
$sql = "SELECT * FROM user WHERE username = '$username'";

正しいユーザ名を提供する代りに、攻撃者は '; DROP TABLE user; -- のような文字列をあなたのアプリケーションに与えることが出来ます。 結果として構築される SQL は次のようになります。

SELECT * FROM user WHERE username = ''; DROP TABLE user; --'

これは有効なクエリで、空のユーザ名を持つユーザを探してから、user テーブルを削除します。 おそらく、ウェブサイトは破壊されて、データは失われることになります (定期的なバックアップは設定済みですよね、ね? )。

Yii においては、ほとんどのデータベースクエリは、PDO のプリペアドステートメントを適切に使用する アクティブレコード を経由して実行されます。 プリペアドステートメントの場合は、上で説明したようなクエリの改竄は不可能です。

それでも、生のクエリクエリビルダ を必要とする場合はあります。 その場合には、データを渡すための安全な方法を使わなければなりません。 データをカラムの値として使う場合は、プリペアドステートメントを使うことが望まれます。

// query builder
$userIDs = (new Query())
    ->select('id')
    ->from('user')
    ->where('status=:status', [':status' => $status])
    ->all();

// DAO
$userIDs = $connection
    ->createCommand('SELECT id FROM user where status=:status')
    ->bindValues([':status' => $status])
    ->queryColumn();

データがカラム名やテーブル名を指定するために使われる場合は、事前定義された一連の値だけを許可するのが最善の方法です。

function actionList($orderBy = null)
{
    if (!in_array($orderBy, ['name', 'status'])) {
        throw new BadRequestHttpException('name と status だけを並べ替えに使うことが出来ます。')
    }

    // ...
}

それが不可能な場合は、テーブル名とカラム名をエスケープしなければなりません。 Yii はそういうエスケープのための特別な文法を持っており、それを使うと、サポートされている全てのデータベースに対して同じ方法でエスケープすることが出来ます。

$sql = "SELECT COUNT([[$column]]) FROM {{table}}";
$rowCount = $connection->createCommand($sql)->queryScalar();

この文法の詳細は、テーブルとカラムの名前を引用符で囲む で読むことが出来ます。

XSS を回避する

XSS すなわちクロスサイトスクリプティングは、ブラウザに HTML を出力する際に、出力が適切にエスケープされていないと発生します。 例えば、ユーザ名を入力できるフォームで Alexander の代りに <script>alert('Hello!');</script> と入力した場合、ユーザ名をエスケープせずに出力している全てのページでは、JavaScript alert('Hello!'); が実行されて、ブラウザにアラートボックスがポップアップ表示されます。 ウェブサイト次第では、そのようなスクリプトを使って、無害なアラートではなく、あなたの名前を使ってメッセージを送信したり、さらには銀行取引を実行したりすることが可能です。

XSS の回避は、Yii においてはとても簡単です。一般に、二つのケースがあります。

  1. データを平文テキストとして出力したい。
  2. データを HTML として出力したい。

平文テキストしか必要でない場合は、エスケープは次のようにとても簡単です。

<?= \yii\helpers\Html::encode($username) ?>

HTML である場合は、HtmlPurifier から助けを得ることが出来ます。

<?= \yii\helpers\HtmlPurifier::process($description) ?>

HtmlPurifier の処理は非常に重いので、キャッシュを追加することを検討してください。

CSRF を回避する

CSRF は、クロスサイトリクエストフォージェリ (cross-site request forgery) の略称です。 多くのアプリケーションは、ユーザのブラウザから来るリクエストはユーザ自身によって発せられたものだと仮定しているけれども、その仮定は間違っているかもしれない ... というのが CSRF の考え方です。

例えば、an.example.com というウェブサイトが /logout という URL を持っており、この URL を単純な GET でアクセスするとユーザをログアウトさせるようになっているとします。 ユーザ自身によってこの URL がリクエストされる限りは何も問題はありませんが、ある日、悪い奴が、ユーザが頻繁に訪れるフォーラムに <img src="http://an.example.com/logout"> というリンクを含むコンテントを何とかして投稿することに成功します。 ブラウザは画像のリクエストとページのリクエストの間に何ら区別を付けませんので、ユーザがそのような img タグを含むページを開くとブラウザはその URL に対して GET リクエストを送信します。 そして、ユーザが an.example.com からログアウトされてしまうことになる訳です。

これは基本的な考え方です。ユーザがログアウトされるぐらいは大したことではない、と言うことも出来るでしょう。 しかし、悪い奴は、この考え方を使って、もっとひどいことをすることも出来ます。 例えば、http://an.example.com/purse/transfer?to=anotherUser&amout=2000 という URL を持つウェブサイトがあると考えて見てください。 この URL に GET リクエストを使ってアクセスすると、権限を持つユーザアカウントから anotherUser に $2000 が送金されるのです。 私たちは、ブラウザは画像をロードするのに常に GET リクエストを使う、ということを知っていますから、この URL が POST リクエストだけを受け入れるようにコードを修正することは出来ます。 しかし残念なことに、それで問題が解決する訳ではありません。 攻撃者は <img> タグの代りに何らかの JavaScript コードを書いて、その URL に対する POST リクエストの送信を可能にすることが出来ます。

CSRF を回避するためには、常に次のことを守らなければなりません。

  1. HTTP の規格、すなわち、GET はアプリケーションの状態を変更すべきではない、という規則に従うこと。
  2. Yii の CSRF 保護を有効にしておくこと。

ファイルの曝露を回避する

デフォルトでは、サーバのウェブルートは、index.php がある web ディレクトリを指すように意図されています。 共有ホスティング環境の場合、それをすることが出来ずに、全てのコード、構成情報、ログをサーバのウェブルートの下に置かなくてはならないことがあり得ます。

そういう場合には、web 以外の全てに対してアクセスを拒否することを忘れないでください。 それも出来ない場合は、アプリケーションを別の場所でホストすることを検討してください。

本番環境ではデバッグ情報とデバッグツールを無効にする

デバッグモードでは、Yii は極めて多くのエラー情報を出力します。これは確かに開発には役立つものです。 しかし、実際の所、これらの饒舌なエラー情報は、攻撃者にとっても、データベース構造、構成情報の値、コードの断片などを曝露してくれる重宝なものです。 本番でのアプリケーションにおいては、決して index.phpYII_DEBUGtrue にして走らせてはいけません。

本番環境では Gii を決して有効にしてはいけません。 Gii を使うと、データベース構造とコードに関する情報を得ることが出来るだけでなく、コードを Gii によって生成したもので書き換えることすら出来てしまいます。

デバッグツールバーは本当に必要でない限り本番環境では使用を避けるべきです。 これはアプリケーションと構成情報の全ての詳細を曝露することが出来ます。 どうしても必要な場合は、あなたの IP だけに適切にアクセス制限されていることを再度チェックしてください。