Improve web application security with Zend Framework 2

Web applications are vulnerable to attacks from different quarters, including SQL injection, XSS, CSRF, spam, and brute-force password hacking. But it's easy to protect your PHP web application from the majority of common attacks with the security-related components available in Zend Framework 2. This article illustrates how to use these components to make your application more secure by validating form input, filtering bot submissions, rejecting comment spam, and logging unusual events.

Defending against malware: A holistic approach to one of today’s biggest IT risks

This article examines the changing strategies that malware has employed in recent years, explains the typical sequence of events that occurs during an attack, and describes how an integrated defense can help keep the enterprise safe from these advanced persistent threats.

Download "Defending against malware."

Introduction

When developing a new website or web application, you can't spend enough time thinking about security. The number of ways in which attackers can worm their way into your web application are numerous, and you need to be rigorous in implementing security best practices in your application code to ensure that your users are not left defenseless.

If you're a PHP developer, this task is made easier by the Zend Framework, which provides a number of ready-to-use components to improve your application's security. In this article, I introduce you to five such components, which let you harden your web application with input validation, output escaping, event logging, spam testing, and bot filtering.

Together, these components serve as a first line of defense in making your application less susceptible to attack. So come on in, and let's get started.


Setting up the base application

Before diving into the code, a few notes and assumptions are in order.

Throughout this article, I assume that you're conversant with the basic principles of application development with Zend Framework 2.x; understand the interaction between actions, controllers, and views; and are familiar with the namespace implementation in PHP 5.3.

I also assume that you have a working Apache/PHP development environment, and that your Apache web server is configured to support virtual hosting and URL rewriting via .htaccess files. In case you're not familiar with these topics, you can find links to more information in the Resources section of this article.

The Zend Framework is a loosely-coupled framework, which means that you can use its components on a stand-alone basis or within its MVC implementation. Given that different projects have different needs, this article attempts to demonstrate usage in both scenarios. Working code examples are also provided in the download archive that accompanies this article.

To begin, set up a standard Zend Framework 2.x application that provides the context for the code shown in this article. You can do this by downloading and using the ZFTool module, as shown here:

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

An alternative approach is to download manually and extract the contents of the Zend Framework skeleton application to a directory on your system. Then, use Composer to download necessary dependencies. The Zend Framework documentation provides additional guidance for this approach; you can find a link to this documentation in the Resources section.

You should now define a new virtual host for this application, such as http://example.localhost, in your Apache configuration, and point the virtual host's document root to the skeleton application's public/ directory. If you then browse to this host, you should see the default Zend Framework 2.x welcome page, as shown in Figure 1.

Figure 1. Zend Framework 2 skeleton application welcome page
Zend Framework 2 skeleton application welcome page

Finally, use ZFTool to create a new controller in the Application module. This controller holds the code examples in this article:

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

With the basics in place, let's dive into some code.


Avoiding injection attacks with input validation

A general rule of web application development is to never blindly trust input supplied by users. Input filtering and validation plays a critical role in blunting injection attacks and should be mandatory for all untrusted input received by a web application.

The Zend Framework provides the Zend\InputFilter component to filter and validate input data, together with a wide range of validators for common use cases. Zend\InputFilter works best with the Zend\Form component, but, as you'll see, you can also use it to validate stand-alone form input.

To begin, create a custom form with a variety of input fields, by extending Zend\Form (Listing 1). I use this form to demonstrate some of the key features of Zend\InputFilter.

Listing 1. Form object
<?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',
        ));  
        
    }
}

Listing 1 sets up a ListingForm object, which represents a form that sellers can use when adding listings to a website. Figure 2 displays the rendered version of this form. Even if you don't know how Zend\Form works, it should be easy for you to map the object in Listing 1 to the standard form elements shown in Figure 2.

Figure 2. Listing form
Listing form

When a user submits a listing, it's important to check the submitted data to ensure that it is valid and does not contain malicious code. This is where the Zend\Filter and Zend\Validate components come in:

  • The Zend\Filter component takes care of scanning input to escape or remove illegal strings (HTML elements or filesystem paths), as well as transforming it to meet formatting requirements (stripping whitespace and newlines or changing string case).
  • The Zend\Validate component ensures that input is valid and matches the expectations of the input requester. This might include checking the format of email addresses, host names, and URIs; ensuring that strings are of a certain minimum length; or validating the precision of numbers and postal codes.

Individual filters and validators can be chained together to test a specific input value, and the value is considered valid only after successfully passing through all the validators and filters in the chain.

The Zend\InputFilter component provides a way to group filters and validators, making it simpler to validate multiple inputs at once - such as, for example, the multiple inputs submitted through a form. Listing 2 uses Zend\InputFilter to create a new ListingFilter object, which contains all the filters and validators needed to test the input submitted through the ListingForm.

Listing 2. Form input validators and filters
<?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'),
            ),
           
        ));        
    }
}

The constructor of the ListingFilter object uses the add() method to specify filtering and validation rules for each of the form input variables. The inputs to the add() method are: the name of the input variable, a flag indicating whether the input is mandatory or not, an array of filters to apply to the input, and an array of validators to apply to the input.

With more than 40 filters and 80 validators to choose from, you're spoilt for choice...and of course, you can also create custom ones (I'll show you an example shortly). Listing 2 uses Zend\Filter\StripTags, which removes any HTML elements from the input, and Zend\Filter\StringTrim, which removes unnecessary whitespace from the input. Validators are selected based on the nature of the input, so Listing 2 uses Zend\Validator\EmailAddress as the most appropriate validator for the 'seller_email' input, and Zend\I18n\Validator\Float to ensure that the 'item_max_price' input is a numeric value.

With the form and filter defined, all that's left is to tie them together in a controller action, as shown in Listing 3.

Listing 3. Form processor
<?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));
    }
}

Listing 3 initializes the ListingForm object, binds it to a custom Listing entity, and attaches the ListingFilter to it. When the form is submitted, the Zend\Form setData() method is used to assign the POST-ed form data to the form object, and the isValid() method takes care of validating the form input using the rules provided in the ListingFilter. Valid data can then be processed further: saved to a database, used in calculations, transmitted to a web service, and so on. In this case, the valid and filtered data is simply printed to the view.

Listing 4 contains the view script, which renders the form.

Listing 4. Form view script
<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); ?>

Figure 3 demonstrates validation in action, with Zend\Form and Zend\InputFilter working together in tandem to highlight invalid input. It also shows the result of a successful form submission.

Figure 3. Form validation and submission
Form validation and submission

In case you're wondering about the Listing entity used in Listing 3, this is a simple convenience. Listings submitted through the ListingForm are bound to a custom Listing entity, which represents each submitted listing as an object and provides getter and setter methods for easier access to object properties. The code for the Listing entity is omitted from this article for brevity, but you can find it in the accompanying code archive.

If the predefined validators don't meet your needs, you can also create your own, either by extending Zend\Validator\AbstractValidator (for complex use cases) or by using Zend\Validator\Callback to run a custom validation function (for simpler use cases). To illustrate, consider Listing 5, which updates the ListingFilter with a validation callback to check that the maximum price is always higher than the minimum price.

Listing 5. Custom validation callback
<?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;
                        },
                    ),
                ),
            ),            
        ));

    }
}

In Listing 5, the callback function inspects the current context, retrieves the value of the 'item_min_price' variable, and compares it with the 'item_max_price' variable to check that the former is less than or equal to the latter. Figure 4 shows it in action.

Figure 4. Form validation with dependent fields
Form validation with dependent fields

Of course, you can also use Zend\InputFilter, Zend\Filter, and Zend\Validate as stand-alone components. To illustrate, consider Listing 6, which contains a simple form with name, age, and credit card number fields.

Listing 6. Form
<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>

Listing 7 sets up validation for the form in Listing 6.

Listing 7. Form processor with filters and validators
<?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/>';
    }
}

Listing 7 begins by setting up the Zend Framework auto-loader, which takes care of loading Zend Framework components as needed. Note that the Zend Framework libraries must be in your PHP include path for this to work.

Once the auto-loader is configured, the next step is to create Zend\Filter\Input objects, one for each input variable, and attach filters and validators to them. Listing 7 uses many of the same filters and validators seen in Listing 2; a notable addition is Zend\Validator\CreditCard, which makes it easy to test credit card numbers for internal consistency.

Once the Input objects are defined, they are grouped together in a Zend\InputFilter object and populated with data from the $_POST super-global. The InputFilter object's isValid() method is then invoked to test the supplied inputs as discussed previously. Valid values can be retrieved with the InputFilter object's getValues() method; invalid inputs are retrieved using the getInvalidInput() method.

Figure 5 displays the result of a valid and invalid form submission.

Figure 5. Form validation and submission
Form validation and submission

Filtering bot requests with CAPTCHAs

Public web forms are also susceptible to form-filling bots, which make thousands of automatic form submissions, usually in the hope of getting spam content into your application database. Bots can also be used to crack user passwords by submitting multiple string combinations to login forms in brute-force attacks.

The easiest way to stop bots in their tracks is by adding a CAPTCHA to your form. The Zend Framework includes a Zend\Captcha component for precisely this task. Zend\Captcha can add figlet or image CAPTCHAs to your form, and it also includes support for the reCAPTCHA web service for remotely-generated CAPTCHAs.

Zend\Captcha usually works best with Zend\Form, as illustrated in Listing 8:

Listing 8. Form object with image 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',
        ));  
        
    }
}

Listing 8 sets up a simple contact form, with inputs for name, email address, and CAPTCHA verification. The CAPTCHA itself is generated with Zend\Captcha\Image, which accepts a number of configuration options, including the length of the CAPTCHA word, the font, and the directory in which to store the generated CAPTCHA. Note that Zend\Captcha\Image uses PHP's GD extension to generate the CAPTCHA image.

Zend\Captcha automatically takes care of setting up the necessary validators, so all that's left is to use it in a controller action (Listing 9).

Listing 9. Form processor
<?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));
    }
}

Figure 6 displays the image CAPTCHA in action.

Figure 6. Form with image CAPTCHA
Form with image CAPTCHA

The default skeleton application does not automatically download ZendService\ReCaptcha, so, if you prefer to use the reCAPTCHA service, you will need to update your application's Composer configuration and add this dependency, as follows:

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

Then, rerun Composer to have the necessary files downloaded and installed to your application.

Listing 10 shows the revised form code using the reCAPTCHA service, and Figure 7 illustrates the revised output. Note that you will need to specify your reCAPTCHA public and private keys as part of the Zend\Captcha\ReCaptcha object configuration. In case you don't have these, look in the Resources section for a link to the reCAPTCHA website, from where you can freely obtain them.

Listing 10. Form with reCAPTCHA integration
<?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
}
Figure 7. Form with reCAPTCHA
Form with reCAPTCHA

If you'd like to use Zend\Captcha on a stand-alone basis, Listing 11 offers an example using the Figlet adapter.

Listing 11. Form and form processor with 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
}
?>

Listing 11 sets up the Zend Framework auto-loader and initializes a new Zend\Captcha\Figlet object. It uses the object's generate() method to generate a new CAPTCHA and identifier and then renders a form with fields for name, message, and CAPTCHA verification. The CAPTCHA itself is also rendered within the form with the object's render() method. When the form is submitted, Zend\InputFilter is used to verify that the entered CAPTCHA value matches the generated string.

Figure 8 displays the form with the rendered CAPTCHA.

Figure 8. Form with figlet CAPTCHA
Form with figlet CAPTCHA

Blocking spam with Akismet

Another way to block form spam is to validate input with the well-known Akismet web service. The ZendService\Akismet component provides a service object that makes it easy to connect to the Akismet API and test whether a particular input is spam or ham.

The default skeleton application does not automatically download ZendService\Akismet, so you need to update your application's Composer configuration and add this dependency, as follows:

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

Listing 12 demonstrates its usage by adding a custom callback for Akismet validation to the MessageForm shown in Listing 8. Note that you need to specify your Akismet API key as part of the ZendService\Akismet object configuration; in case you don't have one, look in the Resources section for a link to the Akismet website, from where you can obtain it for free.

Listing 12. Custom validation callback with Akismet integration
<?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;
                        },
                    ),
                ),            
            )
        ));

    }
}

The custom validation callback in Listing 12 initializes a new ZendService\Akismet object, passing it the Akismet API key and application URL. It then constructs an array of data, as required by the Akismet API. This data includes the IP address and browser of the user submitting the content, together with the user's name and email address. The array is passed to the object's isSpam() method, which contacts the Akismet API and returns a Boolean value indicating whether or not Akismet considers it spam.

Figure 9 shows this validation in action.

Figure 9. Form with Akismet validation
Form with Akismet validation

Blunting XSS attacks with output escaping

Cross-Site Scripting (XSS) is one of the most common attack vectors for a web application. However, by rigorously using output escaping and encoding, you can make your application completely immune to these attacks.

The tool of choice here is the Zend\Escaper component, which provides five methods for escaping output depending on where the output will appear in the rendered page. Methods exist for escaping/encoding content within HTML page content, HTML element attributes, scripting elements, style elements, and URIs.

To illustrate, consider Listing 13, which displays data received from a form submission on a web page. The data consists of a common XSS attack vector listed on the OWASP website; the output is displayed with and without escaping, to illustrate how using Zend\Escaper nullifies the attack.

Listing 13. Output escaping
<?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; ?>

Figure 10 shows the output. As you can see, the unescaped output is executed by the browser to display an alert, in a classic example of an XSS attack.

Figure 10. XSS attack without output escaping
XSS attack without output escaping

In addition to the escapeHtml() method, Zend\Escaper also provides:

  • The escapeHtmlAttr() method for HTML attribute escaping
  • The escapeJs() method for Javascript escaping
  • The escapeCss() method for CSS escaping
  • The escapeUrl() method for URL parameter escaping

Within a Zend Framework application, view helpers for all the previous methods are automatically available within view scripts. In a stand-alone application, you need to instantiate the Zend\Escaper component before using it, as shown in Listing 14.

Listing 14. Output escaping
<?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']);
}

Logging application events and errors

Logs play an important role in application security. They serve as a repository of useful information about how your application is being used, and they can be used to identify or backtrack security breaches and keep audit trails of user activities.

Zend\Log provides a complete framework for logging application events, allowing you to log events to files, databases, email addresses, or browser debuggers. It supports multiple output formats (including your own custom formats) and log levels, allowing precise control over when and how logs are created.

Listing 15 provides a simple example of Zend\Log in action.

Listing 15. Application event logging
<?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;        
    }
}

The controller action in Listing 15 begins by initializing a new Zend\Log\Logger instance; this is the primary control point for interacting with the logging framework. This object is passed a Zend\Log\Writer\Stream object, which is used to write log data to PHP streams or filesystem URLs; in this case, it's initialized with the path to the output log file.

Other writers also exist: you could use a Zend\Log\Writer\Db object to write to any PDO-compliant database, a Zend\Log\Writer\Mail object to send log data via email, a Zend\Log\Writer\MongoDB object to write to a MongoDB database, or a Zend\Log\Writer\FirePHP or Zend\Log\Writer\ChromePHP object to write to the FirePHP or ChromePHP browser console.

The Zend\Log\Writer\Stream object is, in turn, passed a Zend\Log\Formatter\Xml object, which takes care of formatting the log data as XML. As with writers, formatters exist for other targets as well, including database tables, FirePHP and ChromePHP; you can also define your own.

With the logger-writer-formatter chain configured, all that's left is to actually generate a log event. This is easily accomplished with the log() method, which accepts one of eight pre-defined log levels and a message string. Log levels range from 'debug' (debugging messages) to 'emerg' (emergency) and a shortcut method is available for each. Listing 15 uses the info() shortcut method, which is intended for informational messages. There is no need to specify a timestamp with the log message; Zend\Log adds this automatically.

Figure 11 has an example of a sample XML log entry:

Figure 11. XML log output
XML log output

You can also use Zend\Log to log application errors, for later review and rectification. To illustrate, consider Listing 16, which uses Zend\Log as a standalone component with a user-defined PHP error handler, to automatically log application errors.

Listing 16. Listing 16: Error logging
<?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();
?>

Listing 16 uses the set_error_handler() function to override PHP's default error handling mechanism with the user-defined userErrorHandler() function. This function initializes a new Zend\Log logger, attaches a Zend\Log\Writer\Stream writer to it, and then captures error events, including the source file name and line number of the error, to a log file. The message string passed to the logger also includes a dump of the current request object, to aid in debugging. Since no formatter is specified, Zend\Log automatically uses Zend\Log\Formatter\Simple, which produces a single line of output per log event.

Figure 12 shows a sample log entry generated by Listing 16:

Figure 12. Standard log output
Standard log output

Summary

As these examples demonstrate, Zend Framework 2 includes various components that make it easy to secure your web application from injection and XSS attacks, as well as log application events and errors. It's easy to use these components either within Zend Framework's MVC implementation or with a standalone PHP application. You have all the tools you need to start securing your application right away - so don't waste another minute!


Download

DescriptionNameSize
Sample Zend security codezf-security-code.zip12KB

Resources

Learn

Get products and technologies

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Security on developerWorks


  • Bluemix Developers Community

    Get samples, articles, product docs, and community resources to help build, deploy, and manage your cloud apps.

  • Security

    Pragmatic, intelligent, risk-based IT Security practices.

  • DevOps Services

    Software development in the cloud. Register today to create a project.

  • IBM evaluation software

    Evaluate IBM software and solutions, and transform challenges into opportunities.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Security, Open source
ArticleID=964625
ArticleTitle=Improve web application security with Zend Framework 2
publish-date=03042014