Protect Sitecore Media Library with ClamAV antivirus

Posted 9 Sep 2024 by Marek Musielak

protect sitecore media library with clamav antivirus

Have you ever wondered what kind of files are uploaded to Sitecore media library by content authors? Do they run any antivirus checks before uploading them? Do they check them in any way or do they only push what they receive without any thinking if the file is not a malicious one. Or maybe there is an option to upload files to the Sitecore media library from a form on your website? This blog post provides a guide how you can run an antivirus check of any file before it is stored in media library.

I wrote this blog post while working for Blastic, a company that delivers great Sitecore solutions and much more.

First decision you need to make is which antivirus software you want to use. For the purpose of this blog post I decided to use ClamAV which is an open-source antivirus engine with GNU license. Let's go through the steps that will allow to run ClamAV as a windows service and ensure automatic updates of virus databases.

Installing ClamAV as windows service

  • Visit the ClamAV download page.
  • Expand Windows section and choose 64 msi file, e.g. clamav-1.4.1.win.x64.msi.
  • Run the installer and follow the steps to install the application. If you use custom installation folder, use that path in the next steps.
  • Explore C:\Program Files\ClamAV\conf_examples folder and remove .sample from both file names.
  • Edit clamd.conf file and:
    • Remove the Example line.
    • Uncomment the following lines by removing # character:
      LogFile "C:\Program Files\ClamAV\clamd.log"
      LogFileMaxSize 2M
      LogTime yes
      LogClean yes
      LogVerbose yes
      DatabaseDirectory "C:\Program Files\ClamAV\database"
    • If you want only to scan files sent to ClamAV service instead of scanning files on the system, use the following lines for all the drives:
      ExcludePath "C:"
      ExcludePath "D:"
      ExcludePath "E:"
  • Edit freshclam.conf file and:
    • Remove the Example line.
    • Uncomment the following lines by removing # character:
      DatabaseDirectory "C:\Program Files\ClamAV\database"
      UpdateLogFile "C:\Program Files\ClamAV\freshclam.log"
      LogFileMaxSize 10M
      LogTime yes
      LogVerbose yes
      Checks 24
  • Move both clamd.conf and freshclam.conf out of conf_examples to C:\Program Files\ClamAV folder.
  • Run freshclam.exe --install-service as Administrator to install service that will update ClamAV virus databases.
  • Run clamd.exe --install-service as Administrator to install ClamAV service.
  • Start Windows Services by running services.msc command.
  • Find ClamAV FreshClam service and change its startup type to Automatic. Start the service.
  • Check C:\Program Files\ClamAV\database folder - wait until you see main.cvd file there.
  • For troubleshooting, check C:\Program Files\ClamAV\freshclam.log.
  • Find ClamAV ClamD service and change its startup type to Automatic. Start the service.
  • Check C:\Program Files\ClamAV\clamd.log file for any errors.
  • More information on ClamAV documentation website
  • ClamAV antivirus should be now running as a service on your Windows system.

Configure Sitecore to check files attached and uploaded to media library

First, add Nuget reference to nClam package, create an interface and implementation of antivirus service:

namespace MyAssembly.MyNamespace.Services
{
    public interface IAntivirusService
    {
        bool IsFileSafe(Stream stream, string fileName);
    }
}

using System;
using System.IO;
using System.Linq;
using nClam;
using Sitecore.Diagnostics;

namespace MyAssembly.MyNamespace.Services
{
    public class ClamAvAntivirusService : IAntivirusService
    {
        private static readonly string ClamAvServerUrl = Sitecore.Configuration.Settings.GetSetting("ClamAVServer.Url");
        private static readonly int ClamAvServerPort = int.Parse(Sitecore.Configuration.Settings.GetSetting("ClamAVServer.Port"));

        public bool IsFileSafe(Stream stream, string fileName)
        {
            Log.Info($"ClamAV scan started for file {fileName}", this);

            var result = true;

            try
            {
                if (stream != null && stream.Length > 0)
                {
                    using (var ms = new MemoryStream())
                    {
                        if (stream.Position != 0)
                            stream.Position = 0;

                        stream.CopyTo(ms);

                        if (stream.Position != 0)
                            stream.Position = 0;

                        var fileBytes = ms.ToArray();

                        var clam = new ClamClient(ClamAvServerUrl, ClamAvServerPort);

                        var scanResult = clam.SendAndScanFileAsync(fileBytes).Result;

                        switch (scanResult.Result)
                        {
                            case ClamScanResults.Clean:
                                Log.Info($"The file '{fileName}' is clean! ScanResult: {scanResult.RawResult}", this);
                                break;
                            case ClamScanResults.VirusDetected:
                                Log.Error($"Virus Found in file '{fileName}'! Virus name: {scanResult.InfectedFiles?.FirstOrDefault()?.VirusName}", this);
                                result = false;
                                break;
                            case ClamScanResults.Error:
                                Log.Error($"An error occurred while scanning the file '{fileName}'! ScanResult: {scanResult.RawResult}", this);
                                result = false;
                                break;
                            case ClamScanResults.Unknown:
                                Log.Error($"Unknown scan result while scanning the file '{fileName}'! ScanResult: {scanResult.RawResult}", this);
                                break;
                            default:
                                throw new NotImplementedException();
                        }

                    }
                }
            }
            catch (Exception ex)
            {
                Log.Error($"ClamAV Scan Exception while scanning the file '{fileName}'", ex, this);
            }

            Log.Info($"ClamAV scan completed for file {fileName}", this);
            return result;
        }
    }
}

Next, let's create processors that will utilize the service. We want scan to happen on both uploading new files to media library and while attaching new content to an existing media library item:

using MyAssembly.MyNamespace.Services;
using Sitecore;
using Sitecore.Exceptions;
using Sitecore.Pipelines.Attach;

namespace MyAssembly.MyNamespace.Processors.Attach
{
    public class ScanForVirusesOnAttach
    {
        private readonly IAntivirusService _antivirusService;

        public ScanForVirusesOnAttach(IAntivirusService antivirusService)
        {
            _antivirusService = antivirusService;
        }

        public void Process(AttachArgs args)
        {
            if (args?.FileWrapper?.InputStream != null && !_antivirusService.IsFileSafe(args.FileWrapper.InputStream, args.FileWrapper.FileName))
            {
                throw new ClientAlertException(string.Format(Texts.TheFile0HasMaliciousContent, args.FileWrapper.FileName));
            }
        }
    }
}

using MyAssembly.MyNamespace.Services;
using Sitecore;
using Sitecore.Pipelines.Upload;

namespace MyAssembly.MyNamespace.Processors.Upload
{
    public class ScanForVirusesOnUpload
    {
    private readonly IAntivirusService _antivirusService;

        public ScanForVirusesOnUpload(IAntivirusService antivirusService)
        {
            _antivirusService = antivirusService;
        }

        public void Process(UploadArgs args)
        {
            if (args?.Files != null)
            {
                foreach (string key in args.Files)
                {
                    var file = args.Files[key];

                    if (file?.InputStream != null)
                    {
                        if (!_antivirusService.IsFileSafe(file.InputStream, file.FileName))
                        {
                            args.UiResponseHandlerEx.MaliciousFile(StringUtil.EscapeJavascriptString(file.FileName));
                            args.ErrorText = string.Format(Texts.TheFile0HasMaliciousContent, file.FileName);
                            args.AbortPipeline();
                        }
                    }
                }
            }
        }
    }
}

Finally, we have to add configuration file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:env="http://www.sitecore.net/xmlconfig/env/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" >
  <sitecore role:require="Standalone or ContentManagement">
    <settings>
      <setting name="ClamAVServer.Url" 
               value="localhost" />
      <setting name="ClamAVServer.Port" 
               value="3310" />
    </settings>
    <processors>
      <attachFile>
        <processor mode="on"
                   patch:after="processor[@type='Sitecore.Pipelines.Attach.CheckSize,Sitecore.Kernel']"
                   type="MyAssembly.MyNamespace.Processors.Attach.ScanForVirusesOnAttach, MyAssembly"
                   resolve="true">
        </processor>
      </attachFile>
      <uiUpload>
        <processor mode="on"
                   patch:after="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']"
                   type="MyAssembly.MyNamespace.Processors.Upload.ScanForVirusesOnUpload, MyAssembly"
                   resolve="true">
        </processor>
      </uiUpload>
    </processors>
    <services>
      <register serviceType="MyAssembly.MyNamespace.Services.IAntivirusService, MyAssembly"
                implementationType="MyAssembly.MyNamespace.Services.ClamAvAntivirusService, MyAssembly"
                lifetime="Singleton" />
    </services>
    <!-- only needed if reflection filtering is enabled -->
    <reflection>
      <allowedMethods>
        <descriptor type="MyAssembly.MyNamespace.Dialogs.UploadMedia.UploadMediaForm" 
                    methodName="ShowMaliciousFileWarning" 
                    assemblyName="MyAssembly"
                    hint="ShowMaliciousFileWarning" />
      </allowedMethods>
    </reflection>
  </sitecore>
</configuration>

Additionally, if you're using a version of Sitecore older than 10.4, you will need to extend UploadMediaForm with the ShowMaliciousFileWarning method.
Update /sitecore/shell/Applications/Media/Upload Media/UploadMedia.xml and change CodeBeside to:

<CodeBeside Type="MyAssembly.MyNamespace.Dialogs.UploadMedia.UploadMediaForm,MyAssembly"/>

and create that class with code:

using Sitecore;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;

namespace MyAssembly.MyNamespace.Dialogs.UploadMedia
{
    public class UploadMediaForm : Sitecore.Shell.Applications.Media.UploadMedia.UploadMediaForm
    {
        protected void ShowMaliciousFileWarning(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, nameof(fileName));
            SheerResponse.Alert(Texts.TheFile0HasMaliciousContent, fileName);
            OK.Disabled = true;
            Cancel.Disabled = true;
            OK.Disabled = false;
            Cancel.Disabled = false;
        }
    }
}

Having learned how to configure ClamAV for Sitecore, I strongly recommend you apply these measures to secure your media library. Ensuring the protection of your digital assets from malicious threats is vital in the current cybersecurity landscape. Following this guide will help you fortify the safety of your Sitecore system and provide a more secure experience for your users.

Comments? Find me on or Sitecore Chat