ClamAV as a Validation Filter in Zend Framework

Tweet

Ok, so you’re pretty comfortable with using the Zend Framework, specifically the use of Forms. Along with that, you have a good working knowledge of how to combine a host of standard validators such as CreditCard, EmailAddress, Db_RecordExists, and Hex, and standard filters such as Compress/Decompress, BaseName, Encrypt, and RealPath. But what do you do when a situation arises that’s outside the scope of the pre-packaged validators and filters?

Let’s say you want to guard against users uploading files that contain viruses, for example. You would have to write a custom validator that checks the uploads aren’t infected. Today I’ll show you how to do just that – how to write a new file validation filter for Zend Framework that uses ClamAV to ensure uploaded files are virus-free.

Adding ClamAV Support to PHP

First you’ll need to install ClamAV support. I’m basing this installation procedure around Linux, specifically Ubuntu. If you’re using another distribution, you may need to adjust the commands accordingly. Unfortunately, if you’re using Windows however, you’ll need to use a Linux-based Virtual Appliance or setup a virtual machine running Linux to follow along since the php-clamav extension doesn’t support Windows as yet.

Before you attempt to install ClamAv, ensure that you have the library’s dependencies installed. You’ll also want to make sure you have the PHP dev package installed so phpize is available. You can do this by running the following command:

msetter@tango:~$ sudo apt-get install php5-dev libclamav-dev clamav libclamav6 clamav-freshclam

Once you have the dependencies installed, grab a copy of the php-clamav library from sourceforge.net/projects/php-clamav and extract it to a temporary directory on your system. Navigate into the extracted library’s directory and run the following commands:

msetter@tango:~/php-clamav$ phpize
msetter@tango:~/php-clamav$ ./configure --with-clamav
msetter@tango:~/php-clamav$ make

If they all execute without errors, you’ll find a newly compiled module in the modules subdirectory. Copy the module to the directory in which the rest of your PHP modules reside. Your system may vary, but I was able to do it with:

msetter@tango:~/php-clamav$ sudo cp modules/clamav.so /usr/lib/php5/20090626+lfs/

You then need to enable the module in PHP’s configuration file. This is done pretty simply by adding the following line to php.ini and restarting Apache:

extension=clamav.so

Finally, either run php -i from the command line or execute a simple PHP script that contains just a call to phpinfo() to verify the new extension is enabled. You should see output similar to that below.

clamav extension in phpinfo output

The ClamAv library comes with a series of constants and functions, but in this article I will focus on just two functions, cl_scanfile() and cl_pretcode(), as all you need to do is scan the uploaded file and report what the virus is if one is found. For more information on the other available functions visit php-clamav.sourceforge.net.

Building the File Upload Validator

Now that the extension is installed and enabled, let’s get underway and build the Zend Framework ClamAV file upload validator. I’ll assume that you already have a working Zend Framework project which has module support enabled and ready to go. Add support for the new validation library by adding the following line to your application.ini file:

autoloaderNamespaces[] = "Common_"

Then, under the library directory of your Zend Framework project root, create the directory Common/Validate/File and within it a file named ClamAv.php with the following content:

<?php
class Common_Validate_File_ClamAv extends Zend_Validate_Abstract
{
}

With that, your new validator class will be available to the project.

If you’re not familiar with validators in Zend Framework, they’re a pretty straight-forward affair. You can either extend them from Zend_Validate_Abstract or Zend_Validate_Interface. For the purposes of this example, I’m basing the validator on the former. Given that, you will only have to implement two methods: the constructor and isValid().

The constructor should check whether the ClamAv extension is loaded as it’s not shipped with a standard distribution of PHP.

The isValid() method will perform the core work of the validator. Normally the method validates some input and either returns true if the validation was successful or sets an error message in the errors list that is shown afterwards and returns false if the validation failed. Depending on the configuration of your form validators, returning false will either halt the form validation at that point or let the remaining validators continue to run.

Fill out the Common_Validate_File_ClamAv class so it looks like this:

<?php
class Common_Validate_File_ClamAv extends Zend_Validate_Abstract
{
    const STATUS_CLEAN = 0;
    const NOT_READABLE = "fileNotReadable";
    const FILE_INFECTED = "fileInfected";

    protected $_messageTemplates = array(
        self::FILE_INFECTED => "File '%value%' is infected",
        self::NOT_READABLE => "File '%value%' is not readable");

    public function __construct() {
        if (!extension_loaded('clamav')) {
            throw new Zend_Validate_Exception(
                "ClamAv extension is not loaded");
        }
    }
 
    public function isValid($value, $file = null) {
        if ($file === null) {
            $file = array("type" => null, "name" => $value);
        }
        
        if (!Zend_Loader::isReadable($value)) {
            return $this->_throw($file, self::NOT_READABLE);
        }

        $retcode = cl_scanfile($value, $virusname);
        if ($retcode !== self::STATUS_CLEAN) {
            printf("File path: %s | Return code: %s | Virus found name: %s",
                $value, cl_pretcode($retcode), $virusname);
            return $this->_throw($file, self::FILE_INFECTED);
        }
        
        return true;
    }
    
    protected function _throw($file, $errorType) {
        $this->_value = $file["name"];
        $this->_error($errorType);
        return false;
    }
}

First a set of class constants are specified that define the return status for the virus check string templates for custom errors messages. Following that, the constructor checks for ClamAv support being available. If it’s not available, then an exception is thrown.

The isValid() method checks if it the incoming $value argument contains a filename and that the file is readable. If it is, then the cl_scanfile() function is called. The return code from cl_scanfile() indicates whether the file is virus-free. If not, then the name of the virus is retrieved using the cl_pretcode() function and the information is printed.

The _throw() method takes care of setting the appropriate error constant in the class and returning false to indicate that validation has failed. If this happens, the error message linked to the constant will be displayed in the upload form through the use of an error decorator on the input element.

Testing the Validator

With the validator written, you’ll need a form to make use of it and test that it works. Either manually or with zf.sh, create a new action in the IndexController class of the default module and call it “fileUpload”. Add the following code to it:

<?php
class IndexController extends Zend_Controller_Action
{
...
    public function fileUploadAction() {
        $form = new Zend_Form();
        $form->setAction("/default/index/file-upload")
             ->setMethod("post");
    
        $uploadFile = new Zend_Form_Element_File("uploadfile");
        $uploadFile->addValidator(new Common_Validate_File_ClamAv())
           ->setRequired(true)
           ->setLabel("Upload file:");
       
        $form->addElement($uploadFile);
        $form->addElement(new Zend_Form_Element_Submit("submit"));
    
        if ($form->isValid($_POST)) {
            $values = $form->getValues();
            $this->view->messages = array("File uploaded");
        }
    
        $this->view->form = $form;
    }
}

Here you’ve created a simple form and set its action and method properties, a submit button, and a file element. The newly created ClamAv file validator is added to the file element. In addition, the required flag Is set to true ensuring that a file must be uploaded. Following this, both elements are added to the form and a simple if statement checks whether the form has been submitted.

If the form doesn’t validate after being submitted (i.e. the file has a virus), then a validation message will be displayed using the standard error message decorator. Otherwise, a message is added to the view’s messages which will be displayed to the user to indicate the upload was successful.

The last piece is the view script, which is shown below:

<h1>Zend Framework - ClamAV File Upload Validator</h1>
<?php
if (count($this->messages)) {
    echo '<ul id="messages">';
    foreach ($this->messages as $message) {
        echo "<li>" . $this->escape($message) . "</li>";
    }
    echo "</ul>";
}
echo $this->form;

As the lions share of the work already taken care of by the controller and the validator, the view script doesn’t need to do a lot. It simply displays any messages that have been set by the controller and renders the form.

Summary

After working through all that code, you now have a new validator for the Zend Framework that, via the PHP ClamAv library, will check if a file is virus free. I hope that you found this article helpful, both for showing how to create your own custom validators in the Zend Framework and for being able to ensure that you have virus free uploads in the applications that you create from here on in. If you’d like to inspect the code further, code for this article is available for cloning on GitHub.

Image via mathagraphics / Shutterstock

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Michael

    Thank you Matthew, very informative and well presented.

  • http://fiveholiday55.blogspot.com Helen Neely

    Thanks Matthew, for this awesome tutorial. It worked without any problem.

    • http://www.maltblue.com/ Matthew Setter

      Hey there Helen,
      glad to here that it ran without incident. The last thing that I’d want to see is that it broke. I hope you find it useful and am glad you enjoyed the post.

  • http://www.coresoft.de Stefan

    Is it possible to implement and use this validator in Symfony2-Framework?

    • http://www.maltblue.com/ Matthew Setter

      Stefan,
      to be honest, I’m not sure about using it with Symfony 2. But I’m guessing that it wouldn’t be too difficult to implement.

  • http://www.sanalbrain.com Sanal Brain

    thanks :D

  • Alex Gervasio

    Nice and well structured post, Matthew. A good addendum would be wrapping up the whole validation process inside the boundaries of a service or an action controller helper, so boilerplate implementation can be easily consumed across multiple controllers.

    • http://www.maltblue.com/ Matthew Setter

      Alex,
      thank you for the feedback. I’ll look to do that shortly and get the github repository updated. A good idea indeed.

  • David Em

    It’s just too bad there isn’t a PHP extension for Maldetect. http://www.rfxn.com/projects/linux-malware-detect/

  • Gildus

    Excelent application thanks Matthew.

    • http://www.maltblue.com/ Matthew Setter

      Gildus,
      thanks kindly. Glad that you liked it.

  • Kevin

    Excellent! Thanks for sharing!

    • http://www.maltblue.com/ Matthew Setter

      Hey Kevin,
      glad you liked it. What recommendations do you have to take it further?

  • Sebastiaan Stok

    I would not recommend using the ClamAV PHP extension.
    I have had numerous coredumps related to this extension.
    Also, “The problem is the clamav virus database gets loaded into *each* Apache process. That means memory usage per process has jumped from ~16MB, to ~170MB!!”

    Its better to use clamscan directly in combination with clamd.
    The daemon is responsible for the actual scanning and keeping the database in memory. clamdscan then connects to the daemon using an local socket and thus reduces memory usage.
    https://help.ubuntu.com/community/ClamAV#Run_ClamAV_as_a_Daemon

    The returned format can be easily read out using this regexes (from http://www.ijs.si/software/amavisd/ configuration file).
    Nothing found: bOK$
    Fount something: bFOUND$
    Get the actual infections: ^.*?: (?!Infected Archive)(.*) FOUND$

  • http://www.uploadguardian.com Steve

    David Em:
    LMD and Upload Guardian are meant to be used for entire server protection. Both can be configured with mod_security to scan any form of file uploads. Each can be accessed using exec or system commands . LMD is based in shell script and Upload Guardian 1 was Perl, version 2 is Python.
    If there’s interest, I could look at creating a Upload Guardian PHP class.