Last week I took notice of a SDN forum post asking whether there are any best practices for checking files uploaded into Sitecore for viruses.
Although I am unaware of there being any best practices for this, adding a processor into the <uiUpload> pipeline to perform virus scans has been on my plate for the past month. Not being able to find an open source .NET antivirus library has been my major blocker for building one.
However, after many hours of searching the web — yes, I do admit some of this time is sprinkled with idle surfing — over multiple weeks, I finally discovered nClam — a .NET client library for scanning files using a Clam Antivirus server (I setup one using instructions enumerated here).
Before I continue, I would like to caution you on using this or any antivirus solution before doing a substantial amount of research — I do not know how robust the solution I am sharing actually is, and I am by no means an antivirus expert. The purpose of this post is to show how one might go about adding antivirus capabilities into Sitecore, and I am not advocating or recommending any particular antivirus software package. Please consult with an antivirus expert before using any antivirus software/.NET client library .
The solution I came up with uses the adapter design pattern — it basically wraps and makes calls to the nClam library, and is accessible through the following antivirus scanner interface:
using System.IO; namespace Sitecore.Sandbox.Security.Antivirus { public interface IScanner { ScanResult Scan(Stream sourceStream); ScanResult Scan(string filePath); } }
This interface defines the bare minimum methods our antivirus classes should have. Instances of these classes should offer the ability to scan a file through the file’s Stream, or scan a file located on the server via the supplied file path.
The two scan methods defined by the interface must return an instance of the following data transfer object:
namespace Sitecore.Sandbox.Security.Antivirus { public class ScanResult { public string Message { get; set; } public ScanResultType ResultType { get; set; } } }
Instances of the above class contain a detailed message coupled with a ScanResultType enumeration value which conveys whether the scanned file is clean, contains a virus, or something else went wrong during the scanning process:
namespace Sitecore.Sandbox.Security.Antivirus { public enum ScanResultType { Clean, VirusDetected, Error, Unknown } }
I used the ClamScanResults enumeration as a model for the above.
I created and used the ScanResultType enumeration instead of the ClamScanResults enumeration so that this solution can be extended for other antivirus libraries — or calls could be made to other antivirus software through the command-line — and these shouldn’t be tightly coupled to the nClam library.
I then wrapped the nClam library calls in the following ClamScanner class:
using System; using System.IO; using System.Linq; using Sitecore.Diagnostics; using nClam; namespace Sitecore.Sandbox.Security.Antivirus { public class ClamScanner : IScanner { private ClamClient Client { get; set; } public ClamScanner(string server, string port) : this(server, int.Parse(port)) { } public ClamScanner(string server, int port) : this(new ClamClient(server, port)) { } public ClamScanner(ClamClient client) { SetClient(client); } private void SetClient(ClamClient client) { Assert.ArgumentNotNull(client, "client"); Client = client; } public ScanResult Scan(Stream sourceStream) { ScanResult result; try { result = CreateNewScanResult(Client.SendAndScanFile(sourceStream)); } catch (Exception ex) { result = CreateNewExceptionScanResult(ex); } return result; } public ScanResult Scan(string filePath) { ScanResult result; try { result = CreateNewScanResult(Client.SendAndScanFile(filePath)); } catch (Exception ex) { result = CreateNewExceptionScanResult(ex); } return result; } private static ScanResult CreateNewScanResult(ClamScanResult result) { Assert.ArgumentNotNull(result, "result"); if (result.Result == ClamScanResults.Clean) { return CreateNewScanResult("Yay! No Virus found!", ScanResultType.Clean); } if (result.Result == ClamScanResults.VirusDetected) { string message = string.Format("Oh no! The {0} virus was found!", result.InfectedFiles.First().VirusName); return CreateNewScanResult(message, ScanResultType.VirusDetected); } if (result.Result == ClamScanResults.Error) { string message = string.Format("Something went terribly wrong somewhere. Details: {0}", result.RawResult); return CreateNewScanResult(message, ScanResultType.Error); } return CreateNewScanResult("I have no clue about what just happened.", ScanResultType.Unknown); } private static ScanResult CreateNewExceptionScanResult(Exception ex) { string message = string.Format("Something went terribly wrong somewhere. Details:\n{0}", ex.ToString()); return CreateNewScanResult(ex.ToString(), ScanResultType.Error); } private static ScanResult CreateNewScanResult(string message, ScanResultType type) { return new ScanResult { Message = message, ResultType = type }; } } }
Methods in the above class make calls to the nClam library, and return a ScanResult intance containing detailed information about the file scan.
Next I developed the following <uiUpload> pipeline processor to use instances of classes that define the IScanner interface above, and these instances are set via Sitecore when instantiating this pipeline processor — the ClamScanner type is defined in the patch configuration file shown later in this post:
using System.IO; using System.Web; using Sitecore.Diagnostics; using Sitecore.Globalization; using Sitecore.Pipelines.Upload; using Sitecore.Sandbox.Security.Antivirus; namespace Sitecore.Sandbox.Pipelines.Upload { public class ScanForViruses { public void Process(UploadArgs args) { Assert.ArgumentNotNull(args, "args"); foreach (string fileKey in args.Files) { HttpPostedFile file = args.Files[fileKey]; ScanResult result = ScanFile(file.InputStream); if(result.ResultType != ScanResultType.Clean) { args.ErrorText = Translate.Text(string.Format("The file \"{0}\" cannot be uploaded. Reason: {1}", file.FileName, result.Message)); Log.Warn(args.ErrorText, this); args.AbortPipeline(); } } } private ScanResult ScanFile(Stream fileStream) { Assert.ArgumentNotNull(fileStream, "fileStream"); return Scanner.Scan(fileStream); } private IScanner Scanner { get; set; } } }
Uploaded files are passed to the IScanner instance, and the pipeline is aborted if something isn’t quite right — this occurs when a virus is detected, or an error is reported by the IScanner instance. If a virus is discovered, or an error occurs, the message contained within the ScanResult instance is captured in the Sitecore log.
I then glued everything together using the following patch configuration file:
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"> <sitecore> <processors> <uiUpload> <processor mode="on" patch:before="processor[@type='Sitecore.Pipelines.Upload.CheckSize, Sitecore.Kernel']" type="Sitecore.Sandbox.Pipelines.Upload.ScanForViruses, Sitecore.Sandbox"> <Scanner type="Sitecore.Sandbox.Security.Antivirus.ClamScanner"> <param desc="server">localhost</param> <param desc="port">3310</param> </Scanner> </processor> </uiUpload> </processors> </sitecore> </configuration>
I wish I could show you this in action when a virus is discovered in an uploaded file. However, I cannot put my system at risk by testing with an infected file.
But, I can show you what happens when an error is detected. I will do this by shutting down the Clam Antivirus server on my machine:
On upload, I see the following:
When I opened up the Sitecore log, I see what the problem is:
Let’s turn it back on:
I can now upload files again:
If you have any suggestions on making this better, or know of a way to test it with a virus, please share in a comment below.
you can use the standard EICAR Virus test file. http://www.eicar.org/85-0-Download.html
Thanks John!