-
-
Notifications
You must be signed in to change notification settings - Fork 402
Anti-virus support without need to run external programs #1986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
01808ca
92abfda
39b84a6
4de205a
b01b147
951bc8b
144ffae
2c7f3b4
89c38b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -752,6 +752,60 @@ SecDefaultAction "phase:2,log,auditlog,pass" | |
# setvar:'tx.dos_block_timeout=600'" | ||
|
||
|
||
# | ||
# -- [[ Anti-virus Protection ]] ------------------------------------ | ||
# | ||
# Optional anti-virus protection for uploaded files. | ||
# | ||
# Currently, only ClamAV anti-virus is supported. | ||
# | ||
# Prerequisities for this feature: | ||
# - Lua support must be enabled | ||
# - Library lua-socket must be installed in the system | ||
# - modsecurity 'SecTmpSaveUploadedFiles' directive is 'On' or | ||
# the 'SecUploadKeepFiles' directive is set to either | ||
# 'RelevantOnly' or 'On' | ||
# - ngx_http_lua_module web server module (Nginx only, see | ||
# https://github.com/openresty/lua-nginx-module ) | ||
# | ||
# By default, only uploaded files are scanned for viruses. If you | ||
# want to scan also data send by POST/PUT requests (REQUEST_BODY), | ||
# set 'tx.crs_anti-virus_protection_scan_request_body' to 1. | ||
# | ||
# With setting 'tx.crs_anti-virus_clamav_connect_type' you can choose | ||
# between two connection types: | ||
# - tcp: connect using TCP/IP protocol | ||
# - socket: connect using unix socket file | ||
# | ||
# Make sure that setting 'tx.crs_anti-virus_clamav_chunk_size' below | ||
# does not exceed 'StreamMaxLength' as defined in ClamAV | ||
# configuration file clamd.conf. | ||
# | ||
# After enabling, anti-virus scanning should be tested, for example, | ||
# using: | ||
# curl http://localhost --form "data=@eicar.com" | ||
# Get eicar test file from https://www.eicar.org. | ||
# | ||
# Default: no | ||
# Uncomment this rule to change the default. | ||
# | ||
#SecAction \ | ||
# "id:900800,\ | ||
# phase:1,\ | ||
# nolog,\ | ||
# pass,\ | ||
# t:none,\ | ||
# setvar:'tx.crs_anti-virus_protection_enable=1',\ | ||
# setvar:'tx.crs_anti-virus_protection_scan_request_body=0',\ | ||
# setvar:'tx.crs_anti-virus_clamav_connect_type=socket',\ | ||
# setvar:'tx.crs_anti-virus_clamav_socket_file=/var/run/clamav/clamd.ctl',\ | ||
# setvar:'tx.crs_anti-virus_clamav_address=127.0.0.1',\ | ||
# setvar:'tx.crs_anti-virus_clamav_port=3310',\ | ||
# setvar:'tx.crs_anti-virus_clamav_chunk_size=4096',\ | ||
# setvar:'tx.crs_anti-virus_clamav_network_timeout_seconds=2',\ | ||
# setvar:'tx.crs_anti-virus_clamav_max_file_size_bytes=1048576'" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid the FP's in case of libmodsecurity3 (because the
or we can combine these exceptions with checking of CT header. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not count on CT as it can be faked to bypass anti-virus check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May be we should discuss this on monthly chat. I tried this rule in crs-setup.conf:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will keep this open, i think we need to discuss it with others. |
||
|
||
|
||
# | ||
# -- [[ Check UTF-8 encoding ]] ------------------------------------------------ | ||
# | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# ------------------------------------------------------------------------ | ||
# OWASP ModSecurity Core Rule Set ver.3.3.0 | ||
# Copyright (c) 2006-2020 Trustwave and contributors. All rights reserved. | ||
# | ||
# The OWASP ModSecurity Core Rule Set is distributed under | ||
# Apache Software License (ASL) version 2 | ||
# Please see the enclosed LICENSE file for full details. | ||
# ------------------------------------------------------------------------ | ||
|
||
# | ||
# -= Paranoia Level 0 (empty) =- (apply unconditionally) | ||
# | ||
|
||
|
||
|
||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 1" "id:925011,phase:1,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 1" "id:925012,phase:2,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
# | ||
# -= Paranoia Level 1 (default) =- (apply only when tx.executing_paranoia_level is sufficiently high: 1 or higher) | ||
# | ||
|
||
SecRule TX:CRS_ANTI-VIRUS_PROTECTION_ENABLE "!@eq 1" \ | ||
"id:925100,\ | ||
phase:2,\ | ||
pass,\ | ||
nolog,\ | ||
ver:'OWASP_CRS/3.3.0',\ | ||
skipAfter:ANTI-VIRUS_PROTECTION" | ||
|
||
SecRule FILES_TMPNAMES "@inspectFile anti-virus.lua" \ | ||
"id:925110,\ | ||
phase:2,\ | ||
block,\ | ||
t:none,\ | ||
msg:'Virus detected in uploaded file: %{tx.virus_name}',\ | ||
logdata:'Virus detected in uploaded file: %{tx.virus_name}',\ | ||
tag:'paranoia-level/1',\ | ||
tag:'OWASP_CRS',\ | ||
tag:'capec/1000/262/441/442',\ | ||
ver:'OWASP_CRS/3.3.0',\ | ||
severity:'CRITICAL',\ | ||
chain" | ||
SecRule TX:VIRUS_NAME "@streq %{tx.virus_name}" \ | ||
"setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'" | ||
|
||
SecRule TX:CRS_ANTI-VIRUS_PROTECTION_SCAN_REQUEST_BODY "@eq 1" \ | ||
"id:925120,\ | ||
phase:2,\ | ||
block,\ | ||
t:none,\ | ||
msg:'Virus detected in request body: %{tx.virus_name}',\ | ||
logdata:'Virus detected in request body: %{tx.virus_name}',\ | ||
tag:'paranoia-level/1',\ | ||
tag:'OWASP_CRS',\ | ||
tag:'capec/1000/262/441/442',\ | ||
ver:'OWASP_CRS/3.3.0',\ | ||
severity:'CRITICAL',\ | ||
chain" | ||
SecRule REQUEST_BODY "@inspectFile anti-virus.lua" "chain" | ||
SecRule TX:VIRUS_NAME "@streq %{tx.virus_name}" \ | ||
"setvar:'tx.anomaly_score_pl1=+%{tx.critical_anomaly_score}'" | ||
|
||
SecMarker "ANTI-VIRUS_PROTECTION" | ||
|
||
|
||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 2" "id:925013,phase:1,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 2" "id:925014,phase:2,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
# | ||
# -= Paranoia Level 2 =- (apply only when tx.executing_paranoia_level is sufficiently high: 2 or higher) | ||
# | ||
|
||
|
||
|
||
|
||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 3" "id:925015,phase:1,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 3" "id:925016,phase:2,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
# | ||
# -= Paranoia Level 3 =- (apply only when tx.executing_paranoia_level is sufficiently high: 3 or higher) | ||
# | ||
# | ||
|
||
|
||
|
||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 4" "id:925017,phase:1,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
SecRule TX:EXECUTING_PARANOIA_LEVEL "@lt 4" "id:925018,phase:2,pass,nolog,skipAfter:END-REQUEST-925-ANTIVIRUS" | ||
# | ||
# -= Paranoia Level 4 =- (apply only when tx.executing_paranoia_level is sufficiently high: 4 or higher) | ||
# | ||
|
||
|
||
|
||
# | ||
# -= Paranoia Levels Finished =- | ||
# | ||
SecMarker "END-REQUEST-925-ANTIVIRUS" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
-- Binary packing of numbers into big-endian 4 byte integer (>I4), | ||
-- added for compatibility with Lua version < 5.3. | ||
function clamav_pack_chunk_length(x) | ||
local bytes = {} | ||
for j = 1, 4 do | ||
table.insert(bytes, string.char(x % (2 ^ 8))) | ||
x = math.floor(x / (2 ^ 8)) | ||
end | ||
return string.reverse(table.concat(bytes)) | ||
end | ||
|
||
function main(filename_or_data) | ||
pcall(require, "m") | ||
local chunk_size = tonumber(m.getvar("tx.crs_anti-virus_clamav_chunk_size", "none")) | ||
local connect_type = m.getvar("tx.crs_anti-virus_clamav_connect_type") | ||
if connect_type == "socket" then | ||
module_name = "socket.unix" | ||
elseif connect_type == "tcp" then | ||
module_name = "socket" | ||
else | ||
m.log(2, string.format("ClamAV: invalid value '%s' for 'tx.crs_anti-virus_clamav_connect_type' in crs-setup.conf.", connect_type)) | ||
return nil | ||
end | ||
local ok, socket = pcall(require, module_name) | ||
if not ok then | ||
m.log(2, "ClamAV: lua-socket library not installed, please install it or disable 'tx.crs_anti-virus_protection_enable' in crs-setup.conf.") | ||
return nil | ||
end | ||
|
||
local file_handle = io.open(filename_or_data, "r") | ||
if file_handle ~= nil then | ||
data_size = file_handle:seek("end") | ||
file_handle:seek("set", 0) | ||
else | ||
data_size = string.len(filename_or_data) | ||
position_from = 1 | ||
end | ||
-- Empty data, nothing to scan. | ||
if data_size == 0 then | ||
return nil | ||
elseif data_size > tonumber(m.getvar("tx.crs_anti-virus_clamav_max_file_size_bytes")) then | ||
m.log(2, string.format("ClamAV: Scan aborted, data are too big (see 'tx.crs_anti-virus_clamav_max_file_size_bytes' in crs-setup.conf), data size: %s bytes", data_size)) | ||
return nil | ||
end | ||
|
||
if connect_type == "socket" then | ||
sck = socket() | ||
elseif connect_type == "tcp" then | ||
sck = socket.tcp() | ||
end | ||
sck:settimeout(tonumber(m.getvar("tx.crs_anti-virus_clamav_network_timeout_seconds", "none"))) | ||
-- Connecting using unix socket file. | ||
if connect_type == "socket" then | ||
status, error = sck:connect(m.getvar("tx.crs_anti-virus_clamav_socket_file")) | ||
-- Connecting using TCP/IP. | ||
else | ||
status, error = sck:connect(m.getvar("tx.crs_anti-virus_clamav_address", "none"), tonumber(m.getvar("tx.crs_anti-virus_clamav_port", "none"))) | ||
end | ||
if not status then | ||
m.log(2, string.format("ClamAV: Error connecting to antivirus: %s", error)) | ||
return nil | ||
end | ||
|
||
sck:send("nINSTREAM\n") | ||
|
||
-- Data needs to be streamed in chunks prefixed by binary packed chunk size. | ||
-- Streaming is terminated by sending a zero-length chunk. | ||
-- Chunk size is configurable but must NOT exceed StreamMaxLength defined in ClamAV configuration file. | ||
while true do | ||
-- Scanning of uploaded file. | ||
if file_handle ~= nil then | ||
chunk = file_handle:read(chunk_size) | ||
-- Scanning of REQUEST_BODY. | ||
else | ||
if position_from > data_size then | ||
chunk = nil | ||
else | ||
local position_to = position_from + chunk_size | ||
if position_to > data_size then | ||
position_to = data_size | ||
end | ||
chunk = string.sub(filename_or_data, position_from, position_to) | ||
position_from = position_to + 1 | ||
end | ||
end | ||
if chunk then | ||
-- string.pack was introduced in Lua 5.3 but we need to support older versions. | ||
--sck:send(string.pack(">I4", string.len(chunk)) .. chunk) | ||
sck:send(clamav_pack_chunk_length(string.len(chunk)) .. chunk) | ||
else | ||
--sck:send(string.pack(">I4", 0)) | ||
sck:send(clamav_pack_chunk_length(0)) | ||
break | ||
end | ||
end | ||
|
||
if file_handle ~= nil then | ||
io.close(file_handle) | ||
end | ||
|
||
local output = "" | ||
while true do | ||
local s, status, partial = sck:receive() | ||
if s then | ||
output = output .. s | ||
elseif partial then | ||
output = output .. partial | ||
end | ||
if status == "closed" then | ||
break | ||
elseif status == "timeout" then | ||
m.log(2, "ClamAV: Timeout while scanning file.") | ||
return nil | ||
end | ||
end | ||
|
||
sck:close() | ||
|
||
if output == "stream: OK" then | ||
return nil | ||
else | ||
local virus_name = string.match(output, "^stream: (.+) FOUND$") | ||
if virus_name then | ||
m.setvar("tx.virus_name", virus_name) | ||
return string.format("ClamAV: Virus found in uploaded file: %s", virus_name) | ||
else | ||
m.log2(string.format("ClamAV: Unknown response from antivirus: %s", output)) | ||
return nil | ||
end | ||
end | ||
end |
Uh oh!
There was an error while loading. Please reload this page.