Zend Framework 2 を使って Web アプリケーションのセキュリティーを強化する

Web アプリケーションは、SQL インジェクション、XSS、CSRF、スパム、総当たりパスワードによるハッキングを含め、各方面からの攻撃を受けやすいものですが、Zend Framework 2 に用意されているセキュリティー関連のコンポーネントを使用すれば、PHP Web アプリケーションを一般的な攻撃の大多数から簡単に守ることができます。この記事では、これらのコンポーネントを使用して、フォーム入力の検証、ボットによるフォーム送信の除外、コメント・スパムの拒否、そして通常とは異なるイベントのロギングなどを行うことで、アプリケーションをよりセキュアにする方法を説明します。

「Defending against malware (マルウェアに対する防御)」: 現在最大の IT リスクの 1 つとなっているマルウェアに対する総合的なアプローチ

このホワイト・ペーパーでは、マルウェアがここ数年の間に採用したさまざまな戦略について詳しく調べた上で、攻撃中に発生する一連の典型的な事象を説明するとともに、存在し続けるこれらの高度な脅威に対して企業の安全を確保する上で総合防御がいかに有効な手段となり得るかを解説しています。

Defending against malware」をダウンロードしてください。

はじめに

Web サイトや Web アプリケーションを新規に開発するときに、十分な時間をかけてセキュリティーについて検討することはできないものです。アタッカーが Web アプリケーションに忍び込む方法は数え切れないほどあるため、ユーザーが無防備な状態にならないように、アプリケーション・コードにはセキュリティーのベスト・プラクティスを確実に実装しなければなりません。

PHP 開発者の場合、Zend Framework を使用することで、この作業を簡単に行うことができます。Zend Framework には、アプリケーションのセキュリティーを強化するためにすぐに使える数々のコンポーネントが用意されているからです。この記事では、そのようなコンポーネントを 5 つ紹介します。入力検証、出力エスケープ、イベント・ロギング、スパム検査、ボットの除外を行うこれらのコンポーネントを使用することで、Web アプリケーションを強化することができます。

攻撃の影響を受けにくいアプリケーションにする上で、これらのコンポーネントはまとまって防御の最前線として機能します。それでは、説明を始めます。


ベース・アプリケーションのセットアップ

コードの説明に入る前に、いくつかの注意事項と前提条件を説明しておきます。

この記事では一貫して、読者が Zend Framework 2.x を使用したアプリケーション開発の基本原則に精通していること、アクション、コントローラー、ビューの間でのやりとりを理解していること、PHP 5.3 での名前空間の実装に慣れていることを前提とします。

また、実際に使用できる Apache/PHP 開発環境があり、Apache Web サーバーが .htaccess ファイルによって仮想ホスティングと URL リライティングをサポートするように構成されていることも前提とします。以上に挙げた内容についてよく理解していない場合には、この記事の「参考文献」セクションに記載されているリンクから詳しい情報を参照してください。

Zend Framework は疎結合のフレームワークです。つまり、このフレームワークのコンポーネントは、単独で使用することも、フレームワークの MVC 実装に組み込んで使用することもできます。この記事では、プロジェクトによってニーズが異なることを前提に、Zend Framework のコンポーネントを単独で使用する場合と MVC 実装に組み込んで使用する場合の両方のシナリオでの使用例を紹介するつもりです。記事に付属のダウンロード・アーカイブには、実際に動作するサンプル・コードも用意されています。

まず始めに、標準的な Zend Framework 2.x アプリケーションを準備します。このアプリケーションが、記事に記載するコードのコンテキストを提供します。アプリケーションを準備するには、以下に示すように ZFTool モジュールをダウンロードして利用することができます。

shell> php zftool.phar create project example
shell> cd example
shell> php composer.phar install

別の方法として、Zend Framework スケルトン・アプリケーションをダウンロードしてきて、その中身をシステム上のディレクトリーに解凍することもできます。その後、Composer を使用して、必要な依存関係をダウンロードします。この方法については、Zend Framework ドキュメントに詳しいガイダンスが記載されています。このドキュメントへのリンクは、「参考文献」セクションに記載されています。

次に、Apache 構成の中でこのアプリケーションの新しい仮想ホスト (例えば、http://example.localhost/) を定義し、仮想ホストのドキュメント・ルートをスケルトン・アプリケーションの public/ ディレクトリーに指定します。ブラウザーでこのホストにアクセスすると、デフォルトの Zend Framework 2.x ウェルカム・ページが表示されるはずです (図 1 を参照)。

図 1. Zend Framework 2 スケルトン・アプリケーションのウェルカム・ページ
Zend Framework 2 スケルトン・アプリケーションのウェルカム・ページのスクリーン・キャプチャー

最後に、ZFTool を使用して Application モジュールに新規コントローラーを作成します。このコントローラーによって、記事のサンプル・コードが保持されます。

shell> php zftool.phar create controller Example Application example/

基礎となる部分が用意できたので、ここからはコードの詳細を探っていきましょう。


入力を検証してインジェクション攻撃を回避する

Web アプリケーションを開発する上での一般原則は、ユーザーが提供する入力を決して盲目的に信用しないことです。入力のフィルタリングと検証は、インジェクション攻撃を無効にする上で重要な役割を果たすことから、Web アプリケーションが受け付けるすべての信頼できない入力に対して行うことが不可欠です。

Zend Framework には、一般的なケースを対象とした各種バリデーターと併せて、入力データをフィルタリングおよび検証するための Zend\InputFilter コンポーネントが用意されています。Zend\InputFilter は Zend\Form コンポーネントと一緒に使用すると最大限の効果を発揮しますが、フォームの入力を検証するために単独で使用することもできます。

まず、Zend\Form を拡張して、各種の入力フィールドを持つカスタム・フォームを作成します (リスト 1 を参照)。このフォームを使用して、Zend\InputFilter の主要ないくつかの機能をデモしたいと思います。

リスト 1. フォーム・オブジェクト
<?php
namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\Form\Fieldset;
use Application\Entity\Listing;
use Zend\Stdlib\Hydrator\ClassMethods;

class ListingForm extends Form
{
    public function __construct()
    {
        parent::__construct('listing_form');

        $this->setHydrator(new ClassMethods())
             ->setObject(new Listing());    
             
        $this->add(array(
            'name' => 'item_name',
            'options' => array(
                'label' => 'Item name',
            ),
            'attributes' => array(
                'size' => '30'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));

        $this->add(array(
            'name' => 'seller_name',
            'options' => array(
                'label' => 'Seller name',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));
        
        $this->add(array(
            'name' => 'seller_email',
            'options' => array(
                'label' => 'Seller email address',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type' => 'Zend\Form\Element\Email',
        ));

        $this->add(array(
            'name' => 'item_min_price',
            'options' => array(
                'label' => 'Price (min)',
            ),
            'attributes' => array(
                'size' => '5'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));
        
        $this->add(array(
            'name' => 'item_max_price',
            'options' => array(
                'label' => 'Price (max)',
            ),
            'attributes' => array(
                'size' => '5'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));
        
        $this->add(array(
            'name' => 'validity',
            'options' => array(
                'label' => 'Available until',
                'render_delimiters' => false,
                'min_year'  => date('Y'),
                'max_year' => date('Y') + 5
        ),
            'type'  => 'Zend\Form\Element\DateSelect',
        ));  

        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'value' => 'Submit'
            ),
            'type'  => 'Zend\Form\Element\Submit',
        ));  
        
    }
}

リスト 1 で作成している ListingForm オブジェクトは、売り手が商品のリストを Web サイトに追加するときに使用できるフォームを表しています。図 2 に、このフォームをレンダリングしたものを示します。Zend\Form がどのような動作をするか知らなくても、リスト 1 のオブジェクトを図 2 に示されている標準的なフォーム要素に対応させるのは簡単なはずです。

図 2. リストへの追加フォーム
リストへの追加フォームのスクリーン・キャプチャー

ユーザーがリストを送信するときには、送信されるデータをチェックして、それが有効なデータであり、悪意のあるコードが含まれていないことを確認しなければなりません。そこで活躍するのが、Zend\Filter および Zend\Validate コンポーネントです。

  • Zend\Filter コンポーネントは、入力された内容をスキャンして不正な文字列 (HTML 要素やファイル・システムのパスなど) をエスケープまたは削除するだけでなく、フォーマット設定の要件を満たすように入力内容を変更します (ホワイト・スペースと改行を削除したり、文字列の大文字/小文字を変更したりするなど)。
  • Zend\Validate コンポーネントは、入力された内容が妥当であることを確認し、入力を要求する側が期待する内容と突き合わせます。この突き合わせには、e-メール・アドレス、ホスト名、URI などのフォーマットのチェック、文字列が指定の長さ以上であることの確認、数値の精度や郵便番号の検証などが含まれます。

個々のフィルターとバリデーターをチェーンさせて、特定の入力値をテストすることもできます。その場合、チェーンを構成するすべてのバリデーターとフィルターを正常に通過した場合にのみ、値は有効であるとみなされます。

Zend\InputFilter コンポーネントには、フィルターとバリデーターをグループ化する方法が用意されているため、複数の入力 (例えば、フォームから送信された複数の入力など) を一度に検証するのが容易になります。リスト 2 では Zend\InputFilter を使用して、ListingForm から送信された入力をテストするために必要なすべてのフィルターとバリデーターを含めた新しい ListingFilter オブジェクトを作成します。

リスト 2. フォーム入力バリデーターおよびフィルター
<?php
namespace Application\Filter;

use Zend\InputFilter\InputFilter;

class ListingFilter extends InputFilter 
{
    public function __construct() {
    
        $this->add(array(
            'name' => 'item_name',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'StringLength',
                    'options' => array(
                        'min' => 1,
                        'max' => 100,
                    ),
                ),
            ),            
        ));

        $this->add(array(
            'name' => 'item_min_price',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Zend\I18n\Validator\Float',
                ),
                array(
                    'name' => 'GreaterThan',
                    'options' => array(
                      'min' => 0
                    )                    
                ),
            ),            
        ));

        $this->add(array(
            'name' => 'item_max_price',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Zend\I18n\Validator\Float',
                ),
                array(
                    'name' => 'GreaterThan',
                    'options' => array(
                      'min' => 0
                    )                    
                ),
            ),            
        ));
        
        $this->add(array(
            'name' => 'seller_email',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'EmailAddress',
                ),
            ),            
        ));

        $this->add(array(
            'name' => 'seller_name',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
           
        ));        
    }
}

ListingFilter オブジェクトのコンストラクターは add() メソッドを使用して、フォーム入力変数のそれぞれに対するフィルタリング・ルールと検証ルールを指定します。add() メソッドへの入力は、入力変数の名前、入力が必須であるかどうかを示すフラグ、入力に適用するフィルターの配列、入力に適用するバリデーターの配列です。

40 を超えるフィルターと、80 を超えるバリデーターという贅沢な選択肢が揃っていますが、もちろん、カスタムでフィルターやバリデーターを作成することもできます (この後すぐに例を紹介します)。リスト 2 で使用しているのは、入力からすべての HTML 要素を削除する Zend\Filter\StripTags と、不要なホワイト・スペースを入力から削除する Zend\Filter\StringTrim です。バリデーターを選択する際の基準となるのは、バリデーターへの入力が持つ性質であるため、リスト 2 では、'seller_email' 入力に最適なバリデーターとして Zend\Validator\EmailAddress を使用し、Zend\I18n\Validator\Float によって 'item_max_price' 入力が数値であることを確認しています。

フォームとフィルターを定義すれば、その他に必要なことは、これらの要素をコントローラーのアクションの中で結合することだけです (リスト 3 を参照)。

リスト 3. フォーム・プロセッサー
<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\ListingForm;
use Application\Filter\ListingFilter;
use Application\Entity\Listing;

class ExampleController extends AbstractActionController
{
    public function validateAction()
    {
        // generate form and bind to object
        $form = new ListingForm();
        $listing = new Listing();
        $form->bind($listing);

        // set input filters
        $form->setInputFilter(new ListingFilter());
        $request = $this->getRequest();

        // validate and display form input
        if ($request->isPost()) {
            $form->setData($request->getPost());
            if ($form->isValid()) {
                print_r($listing);
                exit;
            }       
        }

        // render view 
        return new ViewModel(array('form' => $form));
    }
}

リスト 3 では、ListingForm オブジェクトを初期化してカスタム Listing エンティティーにバインドした上で、このオブジェクトに ListingFilter をアタッチします。フォームが送信されると、このフォーム・オブジェクトには POST 送信されるフォーム・データが Zend\Form setData() メソッドによって設定されるとともに、isValid() によって、ListingFilter で提供されるルールを使用してフォーム入力が検証されます。データが有効であれば、以降の処理が行われます。つまり、データベースに保存されたり、計算で使われたり、Web サービスに送信されたりするなどの処理です。この例の場合は、有効かつフィルタリングされたデータが、ビューに出力されるだけです。

リスト 4 に記載するビュー・スクリプトは、フォームをレンダリングするスクリプトです。

リスト 4. フォーム・ビュー・スクリプト
<h2>Add Listing</h2>
<?php
// prepare form and set action
$form = $this->form;
$form->prepare();
$form->setAttribute('action', '/application/example/validate');
$form->setAttribute('method', 'post');
echo $this->form()->openTag($form);
?>

<div><?php echo $this->formRow($this->form->get('item_name')); ?></div>

<div><?php echo $this->formRow($this->form->get('item_min_price')); ?></div>

<div><?php echo $this->formRow($this->form->get('item_max_price')); ?></div>

<div><?php echo $this->formRow($this->form->get('seller_name')); ?></div>

<div><?php echo $this->formRow($this->form->get('seller_email')); ?></div>

<div><?php echo $this->formRow($this->form->get('validity')); ?></div>

<div><?php echo $this->formRow($this->form->get('submit')); ?></div>

<?php echo $this->form()->closeTag($form); ?>

図 3 に実際に検証を行った様子を示します。この図では、Zend\Form と Zend\InputFilter の連動により、無効な入力に対してメッセージが表示されています。この図には、フォームが正常に送信された場合の結果も示されています。

図 3. フォームの検証結果と送信結果
フォームの検証結果と送信結果のスクリーン・キャプチャー

リスト 3 で使用されている Listing エンティティーに疑問を感じている方のために言っておきますが、これは単に便宜上使用しているだけです。ListingForm から送信されるリストがバインドされるカスタム Listing エンティティーは、送信された各リストをオブジェクトとして表し、オブジェクトのプロパティーに簡単にアクセスできるようにゲッター・メソッドとセッター・メソッドを提供します。簡単のため、この記事では Listing エンティティーのコードを省略していますが、記事に付属のコード・アーカイブで確認することができます。

定義済みのバリデーターが自分のニーズを満たさない場合には、独自のバリデーターを作成することもできます。それには、Zend\Validator\AbstractValidator を継承するか (ニーズが複雑な場合)、または Zend\Validator\Callback を使用してカスタム検証関数を実行します (ニーズが単純な場合)。これを例で説明するために、リスト 5 について検討してみましょう。このリストでは ListingFilter を更新して、最高価格が常に最低価格より高いことをチェックする検証コールバックを追加しています。

リスト 5. カスタム検証コールバック
<?php
namespace Application\Filter;

use Zend\InputFilter\InputFilter;

class ListingFilter extends InputFilter 
{
    public function __construct() {
    
        // other inputs and validators
        
        $this->add(array(
            'name' => 'item_max_price',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Zend\I18n\Validator\Float',
                ),
                array(
                    'name' => 'GreaterThan',
                    'options' => array(
                      'min' => 0
                    )                    
                ),
                array(
                    'name' => 'Callback',
                    'options' => array(
                        'messages' => array(
                            \Zend\Validator\Callback::INVALID_VALUE => 
                              'The maximum price is less than the minimum price',
                        ),
                        'callback' => function($value, $context=array()) {
                            $maxPrice = $value;
                            $minPrice = $context['item_min_price'];
                            $isValid = $maxPrice >= $minPrice;
                            return $isValid;
                        },
                    ),
                ),
            ),            
        ));

    }
}

リスト 5 のコールバック関数は、現在のコンテキストを検査し、'item_min_price' 変数の値を取得して、それを 'item_max_price' 変数と比較することによって、前者の値が後者の値以下であるかどうかをチェックします。図 4 に、実際にこの検証を行った様子を示します。

図 4. 互いに依存するフィールドを使用したフォームの検証
互いに依存するフィールドを使用したフォームの検証画面のスクリーン・キャプチャー

もちろん、Zend\InputFilter、Zend\Filter、および Zend\Validate を単独のコンポーネントとして使用することもできます。これを例で説明するために、リスト 6 について検討してみましょう。このリストに記載されているのは、名前、年齢、クレジット・カード番号の各フィールドで構成された単純なフォームです。

リスト 6. フォーム
<html>
  <head></head>
  <body>
    <form method="post" action="register.php">
      <div>
        Name <br/>
        <input type="text" name="name" size="30" />
      </div>

      <div>
        Age  <br/>
        <input type="text" name="age" size="3" />
      </div>

      <div>
        Credit card number  <br/>
        <input type="text" name="cnum" size="25" />
      </div>

      <div>
        <input type="submit" name="submit" value="Submit">
      </div>
    
    </form>
  </body>

リスト 7 は、リスト 6 のフォームに対する検証を行うための設定をしています。

リスト 7. フィルターおよびバリデーターを使用したフォーム・プロセッサー
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Input;
use Zend\Validator;
use Zend\I18n;

$name = new Input('name');
$name->getFilterChain()
          ->attachByName('StripTags')
          ->attachByName('StringTrim');
$name->getValidatorChain()
          ->addValidator(new Validator\StringLength(array(
              'min' => '1',
              'max' => '100'
            )));

$age = new Input('age');
$age->getFilterChain()
               ->attachByName('StripTags')
               ->attachByName('StringTrim');
$age->getValidatorChain()
               ->addValidator(new Validator\GreaterThan(array(
                  'min' => '0'
               )))
               ->addValidator(new I18n\Validator\Float());

$cnum = new Input('cnum');
$cnum->getFilterChain()
             ->attachByName('StripTags')
             ->attachByName('StringTrim');
$cnum->getValidatorChain()
             ->addValidator(new Validator\CreditCard());

      
$inputFilter = new InputFilter();
$inputFilter->add($name)
            ->add($age)
            ->add($cnum)
            ->setData($_POST);
            
if ($inputFilter->isValid()) {
    print_r($inputFilter->getValues());
} else {
    echo "The form is not valid.<br/>";
    foreach ($inputFilter->getInvalidInput() as $invalidInput) {
        echo $invalidInput->getName() . ': ' . 
        implode(',',$invalidInput->getMessages()) . '<br/>';
    }
}

リスト 7 は、Zend Framework のオート・ローダーを用意するところから始まります。このローダーは、必要に応じて Zend Framework コンポーネントのロードを行います。このコードを機能させるには、PHP で指定したインクルード・パスに Zend Framework ライブラリーが存在していなければならないことに注意してください。

オート・ローダーを構成した後は、次のステップとして、入力変数ごとに Zend\Filter\Input オブジェクトを作成し、これらのオブジェクトにフィルターとバリデーターをアタッチします。リスト 7 では、リスト 2 に記載されているのと同じフィルターとバリデーターの多くを使用していますが、注目に値するのは、Zend\Validator\CreditCard が追加されていることです。これにより、内部的な一貫性を保つためにクレジット・カード番号のテストをするのが容易になります。

Input オブジェクトの定義が完了すると、これらのオブジェクトがまとめて Zend\InputFilter オブジェクト内にグループ化され、$_POST スーパー・グローバル変数のデータが取り込まれます。それに続いて InputFilter オブジェクトの isValid() メソッドが呼び出され、前に説明したように、提供された入力がテストされます。有効な値は、InputFilter オブジェクトの getValues() メソッドで取得することができます。無効な入力は、getInvalidInput() メソッドを使用して取得されます。

図 5 に、有効なフォーム送信と無効なフォーム送信の結果を示します。

図 5. フォームの検証結果と送信結果
フォームの検証結果と送信結果のスクリーン・キャプチャー

CAPTCHA を使用してボット・リクエストを除外する

公開 Web フォームは、フォーム入力ボットの影響も受けやすいものです。フォーム入力ボットは、通常はスパム・コンテンツをアプリケーション・データベースに忍び込ませることを目的に、自動的に何千ものフォームを送信します。また、ユーザー・パスワードを破ってフォームにログインする目的で、ボットを使用して総当たり攻撃で文字列の組み合わせを送信することも可能です。

ボットの動きを阻止する最も簡単な方法は、フォームに CAPTCHA を追加することです。Zend Framework には、まさにそれを行うための Zend\Captcha コンポーネントが含まれています。Zend\Captcha は、フォームに FIGlet または画像による CAPTCHA を追加できるだけでなく、CAPTCHA がリモートで生成される場合、reCAPTCHA Web サービスのサポートを組み込むこともできます。

リスト 8 に示すように、通常、Zend\Captcha は Zend\Form と一緒に使用するのが最も効果的です。

リスト 8. 画像による CAPTCHA を追加したフォーム・オブジェクト
<?php
namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\Captcha\Image;
use Zend\Captcha\AdapterInterface;

class MessageForm extends Form
{

    protected $captcha;
    
    public function __construct()
    {
        parent::__construct('message_form');

        $this->captcha = new Image(array(
            'expiration' => '300',
            'wordlen' => '7',
            'font' => 'data/fonts/arial.ttf',
            'fontSize' => '20',
            'imgDir' => 'public/captcha',
            'imgUrl' => '/captcha'
        ));
        
        $this->add(array(
            'name' => 'name',
            'options' => array(
                'label' => 'Name',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type'  => 'Zend\Form\Element\Text',
        ));

        $this->add(array(
            'name' => 'email',
            'options' => array(
                'label' => 'Email address',
            ),
            'attributes' => array(
                'size' => '50'
            ),
            'type'  => 'Zend\Form\Element\Email',
        ));
        
        $this->add(array(
            'name' => 'message',
            'options' => array(
                'label' => 'Message',
            ),
            'attributes' => array(
                'rows' => '10',
                'cols' => '50'
            ),
            'type'  => 'Zend\Form\Element\Textarea',
        ));

        $this->add(array(
            'name' => 'captcha',
            'options' => array(
                'label' => 'Verification',
                'captcha' => $this->captcha,
            ),
            'type'  => 'Zend\Form\Element\Captcha',
        ));

        $this->add(array(
            'name' => 'submit',
            'attributes' => array(
                'value' => 'Submit'
            ),
            'type'  => 'Zend\Form\Element\Submit',
        ));  
        
    }
}

リスト 8 で作成しているのは、名前用、e-メール・アドレス用、そして CAPTCHA 検証用の入力フィールドを持つ単純な連絡先フォームです。CAPTCHA 自体は Zend\Captcha\Image を使用して生成されます。Zend\Captcha\Image は、生成する CAPTCHA の語の長さ、フォント、格納ディレクトリーなど、いくつもの構成オプションを受け入れます。Zend\Captcha\Image は、PHP の GD 拡張機能を使用して CAPTCHA の画像を生成することに注意してください。

Zend\Captcha は必要なバリデーターを自動的に作成するため、その後に必要となる作業は、コントローラーのアクションでこれを使用することだけです (リスト 9 を参照)。

リスト 9. フォーム・プロセッサー
<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\MessageForm;
use Application\Filter\MessageFilter;

class ExampleController extends AbstractActionController
{
    
    public function captchaAction()
    {
        $form = new MessageForm();
        $form->setInputFilter(new MessageFilter());
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());
            if ($form->isValid()) {
                print_r($form->getData());
                exit;
            }       
        }
        return new ViewModel(array('form' => $form));
    }
}

図 6 に、画像による CAPTCHA が実際に表示されている様子を示します。

リスト 6. 画像による CAPTCHA が追加されたフォーム
画像による CAPTCHA が追加されたフォームのスクリーン・キャプチャー

ZendService\ReCaptcha は、デフォルトのスケルトン・アプリケーションでは自動的にダウンロードされません。そのため、reCAPTCHA サービスを使用する場合は、アプリケーションの Composer 構成を更新して、以下のように依存関係を追加する必要があります。

{
    // other directives
    "require": {
        "php": ">=5.3.3",
        "zendframework/zendframework": "2.2.*",
        "zendframework/zendservice-recaptcha": "2.*"
    }
}

その後、Composer を再実行すると、必要なファイルがダウンロードされて、アプリケーションにインストールされます。

リスト 10 に、reCAPTCHA サービスを使用して改訂したフォーム・コードを記載し、改訂されたフォームの出力を図 7 に示します。Zend\Captcha\ReCaptcha オブジェクトの構成の一部として、reCAPTCHA の公開鍵と秘密鍵を指定しなければならないことに注意してください。これらの鍵がない場合は、「参考文献」セクションに記載されている reCAPTCHA Web サイトへのリンクを参照すると、そのサイトから無料で入手することができます。

リスト 10. reCAPTCHA を統合したフォーム
<?php
namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\Captcha\ReCaptcha;
use Zend\Captcha\AdapterInterface;

class MessageForm extends Form
{

    protected $captcha;
    
    public function __construct()
    {
        parent::__construct('message_form');  

        $this->captcha = new Recaptcha(array(
            'privKey' => 'YOUR-PRIVATE-KEY',
            'pubKey' => 'YOUR-PUBLIC-KEY',
        ));

        // other elements
}
図 7. reCAPTCHA を統合したフォーム
reCAPTCHA を統合したフォームのスクリーン・キャプチャー

Zend\Captcha を単独で使用したい場合のために、リストと 11 に Figlet アダプターを使用する例を記載します。

リスト 11. FIGlet CAPTCHA を使用したフォームとフォーム・プロセッサー
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Input;
use Zend\Validator;
use Zend\Captcha;
            
$captcha = new Captcha\Figlet(array(
    'name' => 'captcha',
    'expiration' => '300',
    'wordlen' => '7',
));

if ($_POST) {

    $name = new Input('name');
    $name->getFilterChain()
         ->attachByName('StripTags')
         ->attachByName('StringTrim');
    $name->getValidatorChain()
         ->addValidator(new Validator\StringLength(array(
              'min' => '1',
              'max' => '100'
         )));

    $message = new Input('message');
    $message->getFilterChain()
            ->attachByName('StripTags')
            ->attachByName('StringTrim');
    $message->getValidatorChain()
            ->addValidator(new Validator\StringLength(array(
                'min' => '1'
            )));

    $verification = new Input('captcha');
    $verification->getValidatorChain()
                 ->addValidator($captcha);
                
    $inputFilter = new InputFilter();
    $inputFilter->add($name)
                ->add($message)
                ->add($verification)
                ->setData($_POST);
              
    if ($inputFilter->isValid()) {
        print_r($inputFilter->getValues());
    } else {
        echo "The form is not valid.<br/>";
        foreach ($inputFilter->getInvalidInput() as $invalidInput) {
            echo $invalidInput->getName() . ': ' . 
            implode(',',$invalidInput->getMessages()) . '<br/>';
        }
    }
    
} else {

    $id = $captcha->generate();
?>
<html>
  <head></head>
  <body>
    <form method="post" action="message.php">
      <div>
        Name <br/>
        <input type="text" name="name" size="30" />
      </div>

      <div>
        Message <br/>
        <textarea name="message" rows="10" cols="50"></textarea>
      </div>

      <div>
        Verification <br/>
        <pre><?php echo $captcha->getFiglet()->render(
          $captcha->getWord()); ?></pre>
        <input type="hidden" name="captcha[id]" value="<?php echo $id; ?>">
        <input type="text" name="captcha[input]" />
      </div>
      
      <div>
        <input type="submit" name="submit" value="Submit" />
      </div>
    
    </form>
  </body>
<?php
}
?>

リスト 11 では、Zend Framework のオート・ローダーを作成した後、新規 Zend\Captcha\Figlet オブジェクトを作成して初期化しています。そしてこのオブジェクトの generate() メソッドを使用して、新しい CAPTCHA と ID を生成してから、名前用、メッセージ用、そして CAPTCHA 検証用の各フィールドを持つフォームをレンダリングします。オブジェクトの render() メソッドにより、CAPTCHA 自体もフォーム内にレンダリングされます。フォームが送信されるときには、入力された CAPTCHA 値が、生成された文字列と一致するかどうかが Zend\InputFilter によって検証されます。

図 8 に、CAPTCHA がレンダリングされたフォームを示します。

リスト 8. FIGlet CAPTCHA が追加されたフォーム
FIGlet CAPTCHA が追加されたフォームのスクリーン・キャプチャー

Akismet を利用してスパムをブロックする

フォームのスパムをブロックするもう 1 つの方法は、有名な Akismet Web サービスを利用して入力を検証するというものです。ZendService\Akismet コンポーネントが提供するサービス・オブジェクトによって、簡単に Akismet API に接続して、特定の入力がスパムであるかどうかをテストすることができます。

デフォルトのスケルトン・アプリケーションは、ZendService\Akismet を自動的にはダウンロードしないので、アプリケーションの Composer 構成を更新して、以下のように依存関係を追加する必要があります。

{
    // other directives
    "require": {
        "php": ">=5.3.3",
        "zendframework/zendframework": "2.2.*",
        "zendframework/zendservice-akismet": "2.*"
    }
}

リスト 12 では、リスト 8 に記載した MessageForm に Akismet による検証用のカスタム・コールバックを追加することで、Akismet を利用する方法を示しています。ZendService\Akismet オブジェクトの構成の一部として、お持ちの Akismet API キーを指定する必要があることに注意してください。このキーをお持ちでない場合は、「参考文献」セクションに記載されている Akismet Web サイトへのリンクを参照すると、そのサイトから無料で入手することができます。

リスト 12. Akismet を統合したカスタム検証コールバック
<?php
namespace Application\Filter;

use Zend\InputFilter\InputFilter;
use ZendService\Akismet\Akismet;

class MessageFilter extends InputFilter 
{
    public function __construct() {
    
         // other filters and validators

         $this->add(array(
            'name' => 'message',
            'required' => true,
            'filters' => array(
                array('name' => 'StripTags'),
                array('name' => 'StringTrim'),
            ),
            'validators' => array(
                array(
                    'name' => 'Callback',
                    'options' => array(
                        'messages' => array(
                            \Zend\Validator\Callback::INVALID_VALUE => 
                              'The message is spam',
                        ),
                        'callback' => function($value, $context=array()) {
                            $akismet = new Akismet('YOUR-API-KEY', 
                              'http://example.localhost');
                            $data = array(
                                'user_ip' => $_SERVER['REMOTE_ADDR'],
                                'user_agent' => $_SERVER['HTTP_USER_AGENT'],
                                'comment_type' => '',
                                'comment_author' => $context['name'],
                                'comment_author_email' => $context['email'],
                                'comment_content' => $value
                            );
                            return ($akismet->isSpam($data)) ? false : true;
                        },
                    ),
                ),            
            )
        ));

    }
}

リスト 12 のカスタム検証コールバックは、新規 ZendService\Akismet オブジェクトを初期化して、このオブジェクトに Akismet API キーおよびアプリケーションの URL を渡します。次に、Akismet API の要件に従ったデータの配列を作成します。このデータには、フォームの内容を送信するユーザーの IP アドレスとブラウザーの種類を、ユーザーの名前および e-メール・アドレスとともに含めます。配列がオブジェクトの isSpam() メソッドに渡されると、このメソッドが Akismet API にアクセスし、その渡されたデータを Akismet がスパムとみなすかどうかを示すブール値を返します。

図 9 に、この検証を実際に行った様子を示します。

図 9. Akismet による検証を利用したフォーム
Akismet による検証を利用したフォームのスクリーン・キャプチャー

出力をエスケープして XSS 攻撃を無効にする

クロスサイト・スクリプティング (XSS: Cross-Site Scripting) は、Web アプリケーションに対する最も一般的な攻撃手段の 1 つです。ただし、出力のエスケープ処理とエンコード処理を厳格に適用することで、アプリケーションが XSS 攻撃の被害をまったく受けないようにすることができます。

そのための最適なツールは、Zend\Escaper コンポーネントです。このコンポーネントは、レンダリングされるページのどこに出力が表示されるかに応じて、出力をエスケープ処理する 5 つのメソッドを提供します。具体的に言うと、HTML ページのコンテンツ、HTML 要素の属性、スクリプト要素、スタイル要素、URI に含まれる内容をそれぞれエスケープ処理/エンコード処理するための 5 つのメソッドがあります。

これを例で説明するために、リスト 13 について検討してみましょう。このリストのコードは、Web ページでのフォーム送信から受け取ったデータを表示します。このデータは、OWASP Webサイトに記載されている一般的な XSS 攻撃手段で構成されています。リスト 13 では、Zend\Escaper がどのようにして攻撃を無効にするかを説明するために、エスケープ処理をした場合としない場合の出力を表示するようにしています。

リスト 13. 出力のエスケープ処理
<?php
// assume this data came from a request
$data =<<<'EOT'
';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//";
alert(String.fromCharCode(88,83,83))//";alert(String.fromCharCode(88,83,83))//--
></SCRIPT>">'><SCRIPT>alert(String.fromCharCode(88,83,83))</SCRIPT>
EOT;
?>

<h2>Escaped Result</h2>
<?php echo $this->escapeHtml($data); ?>

<h2>Unescaped Result</h2>
<?php echo $data; ?>

図 10 に、上記のコードによる出力を示します。ご覧のように、エスケープ処理されていない出力をブラウザーが実行すると、アラートが表示されます。これは、XSS 攻撃の典型的な例です。

図 10. 出力がエスケープ処理されない場合の XSS 攻撃の結果
出力がエスケープ処理されない場合の XSS 攻撃の結果を示すスクリーン・キャプチャー

escapeHtml() メソッドの他、Zend\Escaper には以下のメソッドも用意されています。

  • HTML 属性をエスケープ処理する escapeHtmlAttr() メソッド
  • JavaScript をエスケープ処理する escapeJs() メソッド
  • CSS をエスケープ処理する escapeCss() メソッド
  • URL パラメーターをエスケープ処理する escapeUrl() メソッド

Zend Framework アプリケーションの内部では、以上のすべてのメソッドに対応するビュー・ヘルパーが、ビュー・スクリプト内で自動的に使用可能になります。単独のアプリケーションの場合は、リスト 14 に示すように、Zend\Escaper コンポーネントを使用する前にインスタンス化する必要があります。

リスト 14. 出力のエスケープ処理
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\Escaper\Escaper;

if ($_POST) {
  $escaper = new Escaper('utf-8');
  echo $escaper->escapeHtml($_POST['message']);
}

アプリケーションのイベントとエラーをログに記録する

アプリケーションのセキュリティーにおいて、ログは重要な役割を果たします。ログは、アプリケーションがどのように使用されているかに関する有用な情報のリポジトリーとして機能するとともに、セキュリティー侵害を特定またはバックトラックするためや、ユーザー・アクティビティーの監査証跡を残すためにも使用することができます。

Zend\Log は、アプリケーションのイベントをログに記録するための完全なフレームワークです。これを使用することで、ファイル、データベース、e-メール・アドレス、またはブラウザー・デバッガーに対するイベントを記録することができます。また、複数の出力フォーマット (独自のカスタム・フォーマットも含まれます) とログ・レベルをサポートしているため、いつ、どのようにログを作成するかを細かく制御することができます。

リスト 15 に、Zend\Log を実際に使用する単純な例を示します。

リスト 15. アプリケーションのイベントのロギング
<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Log\Writer\Stream;
use Zend\Log\Logger;
use Zend\Log\Formatter\Xml;

class ExampleController extends AbstractActionController
{

    // other methods

    public function logAction()
    {
        $logger = new Logger();
        $writer = new Stream('data/log.xml');
        $formatter = new Xml();
        $writer->setFormatter($formatter);
        $logger->addWriter($writer);
        $logger->info('New product added by user');  
        return false;        
    }
}

リスト 15 のコントローラーのアクションは、まず、新規 Zend\Log\Logger インスタンスを初期化するところから始まります。このインスタンスは、このロギング・フレームワークを操作するための主要な制御ポイントです。このオブジェクトには、ログ・データを PHP のストリームまたはファイル・システムの URL に書き込むために使用される Zend\Log\Writer\Stream オブジェクトが渡されます。この Stream オブジェクトは、この例では出力ログ・ファイルのパスで初期化されます。

ライターは他にもあります。使用できるライターとしては、任意の PDO 対応データベースに書き込むための Zend\Log\Writer\Db オブジェクトや、ログ・データを e-メールで送信するための Zend\Log\Writer\Mail オブジェクト、MongoDB データベースに書き込むための Zend\Log\Writer\MongoDB オブジェクト、そして FirePHP あるいは ChromePHP ブラウザー・コンソールにそれぞれ書き込むための Zend\Log\Writer\FirePHP、Zend\Log\Writer\ChromePHP オブジェクトなどがあります。

Zend\Log\Writer\Stream オブジェクトには、ログ・データのフォーマットを XML として設定する Zend\Log\Formatter\Xml オブジェクトが渡されます。ライターと同じく、他のフォーマット (データベース・テーブル、FirePHP、ChromePHP など) をターゲットとしたフォーマッターもあります。さらに、独自のフォーマッターを定義することも可能です。

ロガー、ライター、フォーマッターのチェーンを構成したら、残された作業は実際にログ・イベントを生成することだけです。log() メソッドを使用すれば、簡単にログ・イベントを生成することができます。このメソッドは、8 つある定義済みログ・レベルのいずれか 1 つと、メッセージ・ストリングを受け入れます。ログ・レベルには、「debug」(デバッグ用メッセージ) から「emerg」(緊急) までの範囲があり、レベルごとにショートカット・メソッドが用意されています。リスト 15 で使用している info() ショートカット・メソッドは、情報メッセージ用です。ログ・メッセージにタイム・スタンプを指定する必要はありません。タイム・スタンプは、Zend\Log によって自動的に追加されます。

図 11 に、XML ログ・エントリーの例を示します。

図 11. XML ログ出力
XML ログ出力のスクリーン・キャプチャー

Zend\Log を使用して、後で確認して修正するためにアプリケーションのエラーをログに記録することもできます。これを例で説明するために、リスト 16 について検討してみましょう。このリストでは、自動的にアプリケーション・エラーをログに記録するために、Zend\Log を単独のコンポーネントとして使用し、ユーザー定義の PHP エラー・ハンドラーを組み合わせています。

リスト 16. エラーのロギング
<?php
require_once 'Zend/Loader/StandardAutoloader.php';
$autoloader = new Zend\Loader\StandardAutoloader(array(
    'fallback_autoloader' => true,
));
$autoloader->register();

use Zend\Log\Writer\Stream;
use Zend\Log\Logger;
use Zend\Log\Formatter\Xml;

error_reporting(0);
set_error_handler('userErrorHandler');

function userErrorHandler($errno, $errstr, $errfile, $errline)  {
  $writer = new Stream('error.log');
  $logger = new Logger();
  $logger->addWriter($writer);
  $logger->err("$errstr: $errfile: $errline: " . json_encode($_REQUEST));  
}

// trigger errors
echo 5/0;
some_undef_func();
?>

リスト 16 では set_error_handler() 関数を使用して、PHP のデフォルト・エラー・ハンドリング・メカニズムの代わりにユーザー定義の userErrorHandler() 関数を使用することを指定しています。この関数は、新規 Zend\Log ロガーを初期化し、それに Zend\Log\Writer\Stream ライターをアタッチしてから、エラー・イベント (エラーが発生したソース・ファイル名と行番号を含む) をログ・ファイルに取り込みます。デバッグを支援するために、ロガーに渡されるメッセージ・ストリングにはそのときのリクエスト・オブジェクトのダンプも含まれます。フォーマッターは指定されていないため、Zend\Log は自動的にZend\Log\Formatter\Simple を使用します。このフォーマッターは、ログ・イベントごとに 1 行の出力を生成します。

図 12 に、リスト 16 によって生成されたログ・エントリーの例を示します。

図 12. 標準ログ出力
標準ログ出力のスクリーン・キャプチャー

まとめ

この記事で説明した一連の例から明らかなように、Zend Framework 2 に含まれる各種のコンポーネントによって、Web アプリケーションをインジェクション攻撃や XSS 攻撃から簡単に保護できると同時に、アプリケーションのイベントとエラーを簡単にログに記録することができます。これらのコンポーネントは、Zend Framework の MVC 実装に組み込むにしても、単独の PHP アプリケーションと組み合わせるにしても、簡単に使用することができます。アプリケーションを今すぐ攻撃から保護するためのツールはすべて揃っています。1 分も無駄にしないでください!


ダウンロード

内容ファイル名サイズ
Sample Zend security codezf-security-code.zip12KB

参考文献

学ぶために

製品や技術を入手するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=セキュリティ, Open source
ArticleID=969420
ArticleTitle=Zend Framework 2 を使って Web アプリケーションのセキュリティーを強化する
publish-date=05082014