Tous les systèmes d’exploitation sont vulnérables aux virus et l’upload de fichiers via une application web est une porte d’entrée idéale pour ces derniers.
Aujourd’hui je vous propose un petit tutoriel pour utiliser l’antivirus ClamAV sur un site développé en Symfony2 et qui tourne sur un OS Linux.

Pourquoi ClamAv et pas une autre solution me direz-vous ? Malheureusement les antivirus open-source et gratuits sous Linux ne sont pas légions, j’ai donc opté pour le plus populaire d’entre eux.

Lorsque nous laissons la possibilité aux utilisateurs de pouvoir uploader des fichiers, il est préférable de scanner ces fichiers afin de s’assurer qu’aucun virus n’y est présent.
Nous allons donc voir comment mettre ce contrôle en place.

Installation de ClamAv

Commençons par le commencement, l’installation de l’antivirus.

Voici la liste des modules à installer :

  • clamav
  • clamav-daemon
  • clamav-freshclam

Pour ce faire, sous Debian, exécutez les commandes suivantes (adapter en fonction de la distribution utilisée) :

apt-get install clamav
apt-get install clamav-daemon
apt-get install clamav-freshclam

Une fois les 3 modules installés, une mise à jour de la base de virus s’impose. Pour cela, lancez la commande suivante :

freshclam

Si vous voulez avoir plus d’informations quant à l’installation ou l’utilisation de l’outil en ligne de commande, je vous invite à vous rendre sur cette documentation : https://doc.ubuntu-fr.org/clamav

Intégration à Symfony2

Commençons par créer une interface qui décrira la signature attendue de classe chargée de piloter l’antivirus.


#src/MyProject/CoreBundle/Services/Interfaces/AntivirusInterface.php

namespace MyProject\CoreBundle\Services\Interfaces;

/**
 * Interface des services permettant l'analyse antivirus de fichiers
 */
interface AntivirusInterface
{
    /**
     * Vérifie qu'un fichier est sain en fournissant son chemin d'accès
     *
     * @param string  $filePath     Chemin du fichier à contrôler
     * @param boolean $removeUnsafe Supprimer automatiquement le fichier si celui-ci n'est pas sûr ? (default => false)
     * @return boolean
     */
    public function isFileSafe($filePath, $removeUnsafe = false);
}

Ecrivons ensuite la classe implémentant cette interface.
Cette implémentation pilotera l’utilitaire en ligne de commande via le composant Process de Symfony2.

Libre à vous d’adapter cette implémentation pour piloter un autre antivirus, il suffit de modifier la commande liée.


#src/MyProject/CoreBundle/Services/ClamAvService.php

namespace MyProject\CoreBundle\Services;

use MyProject\CoreBundle\Services\Interfaces\AntivirusInterface;
use Symfony\Component\Process\Process;

/**
 * Service pilotant l’antivirus ClamAvService pour vérifier
 * la présence de virus dans un fichier
 */
class ClamAvService implements AntivirusInterface
{
    /**
     * Vérifie qu'un fichier est sain en fournissant son chemin d'accès
     *
     * @param string  $filePath     Chemin du fichier à contrôler
     * @param boolean $removeUnsafe Supprimer automatiquement le fichier si celui-ci n'est pas sûr
     * @return boolean
     */
    public function isFileSafe($filePath, $removeUnsafe = false)
    {
        $process = new Process('clamdscan "' . $filePath . '"');
        $process->setTimeout(10);
        $process->run();

        // On supprime le fichier si virus détecté + demande explicite de suppression
        if ($removeUnsafe && !$process->isSuccessful()) {
            unlink($filePath);
        }

        return $process->isSuccessful();
    }
}

On déclare ensuite notre service pour pouvoir l’utiliser en tant que tel.


#src/MyProject/CoreBundle/Resources/config/services.yml

antivirus.service:
    class: MyProject\CoreBundle\Services\ClamAvService

Je vous propose ensuite de déclarer une classe StringUtils qui va nous permettre de « slugifier » le nom du fichier que l’on vient d’uploader. Cela évitera de se retrouver avec des noms contenant des caractères spéciaux qui nous compliqueront la tâche par la suite.


#src/MyProject/CoreBundle/UtilStringUtils.php

namespace MyProject\CoreBundle\Util;

/**
 * String utility functions.
 */
class StringUtils
{
    /**
     * Permet de retirer tous les caractères spéciaux d'une chaine
     *
     * @param  string $slug Chaine à traiter
     * @return string Même chaine sans les caractères spéciaux
     */
    public static function slugify($text = null)
    {
        // remplace les caractères spéciaux par des -
        $text = preg_replace('~[^\\pL\d]+~u', '-', $text);

        // trim
        $text = trim($text, '-');

        // transliteration
        $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

        // minuscule
        $text = strtolower($text);

        // suppression des caractères indésirables
        $text = preg_replace('~[^-\w]+~', '', $text);

        if (empty($text)) {
            return 'n-a';
        }

        return $text;
    }
}

Tout est prêt !
Voyons maintenant comment utiliser ces différents composants.

Admettons que vous ayez un formulaire avec 2 champs : un titre et une pièce jointe.
Une fois le formulaire validé (c’est-à-dire que le champ titre est bien renseigné et que le document à bien été renseigné par exemple), le code suivant permet de vérifier le fichier uploadé.


<?php
// $file est une instance de Symfony\Component\HttpFoundation\File\UploadedFile qui représente le fichier uploadé
$directory = "/var/www/mon/chemin/de/stockage";


// On ‘slugifie’ le nom du fichier
$newName = StringUtils::slugify(pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME)) .
    '.' . pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION);

// On place le fichier dans le bon répertoire
$file->move($directory, $newName);

// On vérifie que le fichier est sûr, dans le cas échant on le supprime
if ($this->get('antivirus.service')->isFileSafe($directory . $newName, true)) {
    // FICHIER OK – On continue les traitements
} else {
    // FICHIER CORROMPU – Il a déjà été supprimé, il faut informer l’utilisateur
}

La méthode « isFileSafe() » est en charge de vérifier la présence ou non d’un virus dans le fichier uploadé. Si celui-ci est corrompu, il est directement supprimé.

Voilà un moyen simple mais efficace de se protéger contre les virus sur un projet Symfony2 !