使用Zend Framework 2提高Web应用程序的安全性

在开发新网站或Web应用程序时,您不能花费足够的时间来考虑安全性。 攻击者可以多种方式入侵您的Web应用程序,并且您需要严格地在应用程序代码中实现安全性最佳实践,以确保用户免受攻击。

如果您是PHP开发人员,那么Zend Framework将使此任务更加容易,Zend Framework提供了许多现成的组件来提高应用程序的安全性。 在本文中,我向您介绍了五个这样的组件,它们使您可以通过输入验证,输出转义,事件日志记录,垃圾邮件测试和漫游器筛选来强化Web应用程序。

这些组件共同构成了使您的应用程序更不容易受到攻击的第一道防线。 因此,进入,让我们开始吧。

设置基础应用程序

在深入研究代码之前,需要注意一些注意事项和假设。

在本文中,我假设您已经熟悉Zend Framework 2.x进行应用程序开发的基本原理; 了解动作,控制器和视图之间的交互; 并且熟悉PHP 5.3中的名称空间实现。

我还假设您有一个正常的Apache / PHP开发环境,并且您的Apache Web服务器配置为支持虚拟主机和通过.htaccess文件进行URL重写。 如果您不熟悉这些主题,可以在本文的“ 相关主题”部分中找到指向更多信息的链接。

Zend框架是一个松耦合的框架,这意味着您可以独立使用它的组件,也可以在其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在“应用程序”模块中创建一个新的控制器。 此控制器保存本文中的代码示例:

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

掌握了基础知识之后,让我们深入研究一些代码。

通过输入验证避免注入攻击

Web应用程序开发的一般规则是永远不要盲目地信任用户提供的输入。 输入过滤和验证在钝化注入攻击中起着至关重要的作用,对于Web应用程序收到的所有不受信任的输入都应强制使用输入过滤和验证。

Zend框架提供了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对象,该对象表示卖家在将列表添加到网站时可以使用的表单。 图2显示了此表单的呈现版本。 即使您不知道Zend \ Form的工作原理,也很容易将清单1中的对象映射到图2所示的标准表单元素。

图2.清单表单
上市表格

当用户提交列表时,检查提交的数据以确保其有效且不包含恶意代码很重要。 这是Zend \ Filter和Zend \ Validate组件进入的位置:

  • Zend \ Filter组件负责扫描输入以转义或删除非法字符串(HTML元素或文件系统路径),并对其进行转换以符合格式要求(去除空格和换行符或更改字符串大小写)。
  • Zend \ Validate组件可确保输入有效并匹配输入请求者的期望。 这可能包括检查电子邮件地址,主机名和URI的格式; 确保字符串具有一定的最小长度; 或验证数字和邮政编码的精度。

可以将各个过滤器和验证器链接在一起以测试特定的输入值,并且只有成功通过链中的所有验证器和过滤器后,该值才被视为有效。

Zend \ InputFilter组件提供了一种对过滤器和验证器进行分组的方法,从而使一次验证多个输入(例如,通过表单提交的多个输入)更简单。 清单2使用Zend \ InputFilter创建一个新的ListingFilter对象,该对象包含测试通过ListingForm提交的输入所需的所有过滤器和验证器。

清单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使用Zend \ Filter \ StripTags从输入中删除所有HTML元素,以及Zend \ Filter \ StringTrim从输入中删除不必要的空格。 根据输入的性质选择验证器,因此清单2使用Zend \ Validator \ EmailAddress作为'seller_email'输入的最合适的验证器,并使用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附加到它。 提交表单后,将使用Zend \ Form setData()方法将POST版本的表单数据分配给表单对象,而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实体,该实体将每个提交的清单表示为一个对象,并提供getter和setter方法,以便于访问对象属性。 为简洁起见,本文省略了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组件。 请注意,Zend Framework库必须在您PHP包含路径中才能起作用。

一旦配置了自动加载器,下一步就是创建Zend \ Filter \ Input对象,每个输入变量一个,并将过滤器和验证器附加到它们。 清单7使用了许多与清单2相同的过滤器和验证器; 值得注意的是Zend \ Validator \ CreditCard,这使得测试信用卡号的内部一致性很容易。

定义Input对象后,它们将在Zend \ InputFilter对象中分组在一起,并使用$_POST超全局$_POST数据进行填充。 然后,调用InputFilter对象的isValid()方法来测试所提供的输入,如前所述。 可以使用InputFilter对象的getValues()方法检索有效值。 使用getInvalidInput()方法检索无效的输入。

图5显示了有效和无效表单提交的结果。

图5.表单验证和提交
表单验证和提交

使用验证码过滤机器人请求

公共Web表单也容易受到表单填充机器人的攻击,该机器人会自动提交数千个表单,通常是希望将垃圾内容输入到您的应用程序数据库中。 通过在暴力攻击中提交多个字符串组合以登录表单,Bot也可以用于破解用户密码。

使机器人停止前进的最简单方法是在表单中添加一个验证码。 Zend框架包括一个Zend \ Captcha组件,可以精确地完成此任务。 Zend \ Captcha可以将figlet或图像CAPTCHA添加到您的表单中,并且还支持对远程生成的CAPTCHA的reCAPTCHA Web服务。

Zend \ Captcha通常与Zend \ Form一起使用时效果最好,如清单8所示:

清单8.具有图像验证码的表单对象
<?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设置了一个简单的联系表单,其中包含姓名,电子邮件地址和CAPTCHA验证的输入。 CAPTCHA本身是由Zend \ Captcha \ Image生成的,它接受许多配置选项,包括CAPTCHA字的长度,字体以及用于存储生成的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显示了运行中的验证码图像。

图6.带有图像验证码的表单
带有图片验证码的表格

默认的框架应用程序不会自动下载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网站的链接,您可以从该网站免费获取它们。

清单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和标识符,然后呈现带有用于名称,消息和CAPTCHA验证的字段的表单。 CAPTCHA本身也通过对象的render()方法呈现在表单中。 提交表单后,将使用Zend \ InputFilter验证输入的CAPTCHA值是否与生成的字符串匹配。

图8显示带有呈现的CAPTCHA的表单。

图8.带figlet CAPTCHA的表单
带有figlet CAPTCHA的表单

用Akismet阻止垃圾邮件

阻止表单垃圾邮件的另一种方法是使用著名的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验证的自定义回调来演示其用法。 注意,您需要在ZendService \ Akismet对象配置中指定Akismet API密钥。 如果您没有,请在“ 相关主题”部分中找到指向Akismet网站的链接,您可以从该网站免费获取该链接。

清单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地址和浏览器,以及用户的名称和电子邮件地址。 该数组传递给对象的isSpam()方法,该方法与Akismet API联系并返回一个布尔值,该值指示Akismet是否将其视为垃圾邮件。

图9显示了这种验证的实际效果。

图9.带有Akismet验证的表单
带有Akismet验证的表格

输出转义,钝化XSS攻击

跨站点脚本(XSS)是Web应用程序最常见的攻击媒介之一。 但是,通过严格使用输出转义和编码,可以使您的应用程序完全不受这些攻击。

这里选择的工具是Zend \ Escaper组件,该组件提供了五种转义输出的方法,具体取决于输出将在呈现的页面中出现的位置。 存在用于在HTML页面内容,HTML元素属性,脚本元素,样式元素和URI中转义/编码内容的方法。

为了说明这一点,请考虑清单13,该清单显示了从网页上的表单提交接收到的数据。 数据由OWASP网站上列出的通用XSS攻击媒介组成; 显示输出时是否带有转义符,以说明使用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 \ escapeHtml()还提供:

  • HTML属性转义的escapeHtmlAttr()方法
  • 用于转义Javascript的escapeJs()方法
  • CSS转义的escapeCss()方法
  • URL参数转义的escapeUrl()方法

在Zend Framework应用程序中,所有先前方法的视图助手在视图脚本中自动可用。 在独立应用程序中,您需要在使用之前实例化Zend \ Escaper组件,如清单14所示。

清单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提供了一个完整的框架来记录应用程序事件,使您可以将事件记录到文件,数据库,电子邮件地址或浏览器调试器中。 它支持多种输出格式(包括您自己的自定义格式)和日志级别,从而可以精确控制何时以及如何创建日志。

清单15提供了一个简单的Zend \ Log in操作示例。

清单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实例开始; 这是与日志记录框架进行交互的主要控制点。 向该对象传递Zend \ Log \ Writer \ Stream对象,该对象用于将日志数据写入PHP流或文件系统URL。 在这种情况下,它将使用输出日志文件的路径进行初始化。

还存在其他编写者:您可以使用Zend \ Log \ Writer \ Db对象写入任何符合PDO的数据库,使用Zend \ Log \ Writer \ Mail对象通过电子邮件发送日志数据,使用Zend \ Log \ Writer \ MongoDB对象写入MongoDB数据库,或者Zend \ Log \ Writer \ FirePHP或Zend \ Log \ Writer \ ChromePHP对象写入FirePHP或ChromePHP浏览器控制台。

Zend \ Log \ Writer \ Stream对象又传递了Zend \ Log \ Formatter \ Xml对象,该对象负责将日志数据格式化为XML。 与编写器一样,其他目标也有格式化程序,包括数据库表,FirePHP和ChromePHP。 您也可以定义自己的。

配置了logger-writer-formatter链后,剩下的就是实际生成一个日志事件。 这可以通过log()方法轻松实现,该方法接受八个预定义的日志级别之一和一个消息字符串。 日志级别范围从'debug' (调试消息)到'emerg' (紧急),并且每种方法都有一种快捷方法。 清单15使用info()快捷方式方法,该方法用于参考消息。 无需为日志消息指定时间戳; Zend \ Log会自动添加它。

图11给出了一个示例XML日志条目的示例:

图11. XML日志输出
XML日志输出

您还可以使用Zend \ Log记录应用程序错误,以供以后查看和纠正。 为了说明这一点,请考虑清单16 ,该清单使用Zend \ Log作为具有用户定义PHP错误处理程序的独立组件,来自动记录应用程序错误。

清单16.清单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()函数通过用户定义的userErrorHandler()函数覆盖PHP的默认错误处理机制。 此函数初始化一个新的Zend \ Log记录器,将一个Zend \ Log \ Writer \ Stream写入器附加到它,然后将错误事件(包括错误的源文件名和行号)捕获到日志文件中。 传递给记录器的消息字符串还包括当前请求对象的转储,以帮助调试。 由于未指定格式化程序,因此Zend \ Log自动使用Zend \ Log \ Formatter \ Simple,每个日志事件产生一行输出。

图12显示了清单16生成的示例日志条目:

图12.标准日志输出
标准日志输出

摘要

如这些示例所示,Zend Framework 2包含各种组件,这些组件使保护Web应用程序免受注入和XSS攻击以及记录应用程序事件和错误变得容易。 在Zend Framework的MVC实现中或与独立PHP应用程序一起使用这些组件很容易。 您拥有立即开始保护应用程序所需的所有工具-因此不要浪费时间!


翻译自: https://www.ibm.com/developerworks/opensource/library/se-zend-security/index.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值