ClamAV LUA script for ModSecurity

April 16, 2017 technews

I started to use ModSecurity in 2004 when I needed a way to protect application like PhpMyAdmin and the SquirrelMail from the classic SQL Injection attacks that was popular in that years. ModSecurity, fail2ban, monit and some custom-made scripts that correlates logs (and attacks) are part of the basic toolkit that I use in every web server to guarantee the uptime of a web server.

In these days I’m working on ModSecurity to update my infrastructure to the most recent release of the module and the Core RuleSet and I decided to implement the AV scan of the uploaded contents.
In the past some customers used a Perl script that uses clamdscan (or clamscan as failback solution) but this solution has the drawback that Apache needs to start every time an external Perl interpreter (a time- and memory-consuming task) to launch another external shell to complete the scan.
ModSecurity supports an embedded programming language called Lua to reduce the latency introduced by the external shell and uses the LuaJIT to compile and execute the script, so it’s time to convert the virusscan Perl script to a Lua language.

Here you can find the updated version:

   This script can be used to inspect uploaded files for viruses
   via ClamAV. To implement, use with the following ModSecurity rule:

   SecRule FILES_TMPNAMES "@inspectFile /opt/modsecurity/bin/modsec-clamscan.lua" "phase:2,t:none,log,deny"

   Author: Angelo Conforti (based on Josh Amishav-Zlatin code)
   Requires the clamav-server and clamav-scanner
   If you use SELinux on RHEL base distro:
   setsebool -P antivirus_can_scan_system 1
   And remember that CentOS ClamAV distribution has some issue 
   with permission in the "default" configuration. Use Debian and
   you'll be happy :)

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <>.

function fsize(filename)
   file =,"r")
   local current = file:seek()
   local size = file:seek("end")
   return size

function main(filename)
   -- Configure paths
   local clamdscan  = "/usr/bin/clamdscan"
   local clamscan  = "/usr/bin/clamscan"
   -- failoverOnClamdFailure: failover to clamscan if clamdscan report an error
   local failoverOnClamdFailure = true
   -- fail (and block) if clamdscan (and clamscan) fails
   local failOnError = false
   -- local var
   local agent = "clamdscan"

   -- Skip empty items because if clamd is not working and you
   -- use the clamscan agent an empty file can take about 12 secs 
   -- to be analyzed
   if fsize(filename) == 0 then
     m.log(1, "[scanav skipped, file " .. filename .." size is zero]")
     return nil

   -- The system command we want to call with fdpass flag to 
   -- do not incur in a permission issue
   local cmd = clamdscan .. " --fdpass --stdout --no-summary"

   -- Run the command and get the output
   local f = io.popen(cmd .. " " .. filename .. " || true")
   local l = f:read("*a")

   -- Check the output for the FOUND or ERROR strings which indicate
   -- an issue we want to block access on
   local isVuln = string.find(l, "FOUND")
   local isError = string.find(l, "ERROR")

   -- If clamdscan fails and you want failover to the traditional clamscan...
   if isError and failoverOnClamdFailure then
     -- Try to use the clamscan program
     m.log(1, "[clamdscan fails (" .. l .. "), failover to clamscan]")
     agent = "clamscan"
     cmd = clamscan .. " --stdout --no-summary"
     f = io.popen(cmd .. " " .. filename .. " || true")
     l = f:read("*a")
     isVuln = string.find(l, "FOUND")
     isError = string.find(l, "ERROR")

   if isVuln then
     m.log(1, "[" .. agent .. " scanner message: " .. l .. "]")
     return "Virus Detected"
   elseif isError and failOnError then
     -- is a error (not a virus) a failure event?
     m.log(1, "[" .. agent .. " scanner message: " .. l .. "]")
     return "Error Detected"
     return nil

The script can be configured to fit your needs with these variables:

  • failoverOnClamdFailure: the default scan engine is the clamdscan that uses the clamd daemon to speedup the scan procedure; clamd loads the virus definitions files at the startup and clamdscan pass the file handle (–fdpass, to avoid issues with selinux) to the daemon to scan the file in less than a second. If clamdscan fails because clamd is not running you can choose if failover to clamscan (slower) or fail
  • failOnError: set what happend if clamdscan (and clamscan if failover is active) fails: report a definitive error to modsecurity, that means the request will be blocked, or ignore the failure and permit the non-verified upload

Questo post รจ disponibile anche in: Italiano