Browse Source

Rules updates

tags/1.4.1
Steve Freegard 7 years ago
parent
commit
5c669479a0
4 changed files with 713 additions and 0 deletions
  1. 582
    0
      rules/misc.lua
  2. 91
    0
      rules/regexp/headers.lua
  3. 39
    0
      rules/regexp/misc.lua
  4. 1
    0
      rules/rspamd.lua

+ 582
- 0
rules/misc.lua View File

@@ -395,3 +395,585 @@ rspamd_config.MISSING_FROM = {
group = 'header',
description = 'Missing From: header'
}

rspamd_config.RCVD_HELO_USER = {
callback = function (task)
-- Check HELO argument from MTA
local helo = task:get_helo()
if (helo and helo:lower():find('^user$')) then
return true
end
-- Check Received headers
local rcvds = task:get_header_full('Received')
if not rcvds then return false end
for _, rcvd in ipairs(rcvds) do
local r = rcvd['decoded']:lower()
if (r:find("^%s*from%suser%s")) then return true end
if (r:find("helo[%s=]user[%s%)]")) then return true end
end
end,
description = 'HELO User spam pattern',
score = 3.0
}

rspamd_config.URI_COUNT_ODD = {
callback = function (task)
local ct = task:get_header('Content-Type')
if (ct and ct:lower():find('^multipart/alternative')) then
local urls = task:get_urls()
if (urls and (#urls % 2 == 1)) then
return true
end
end
end,
description = 'Odd number of URIs in multipart/alternative message',
score = 1.0
}

rspamd_config.HAS_ATTACHMENT = {
callback = function (task)
local parts = task:get_parts()
if parts and #parts > 1 then
for _, p in ipairs(parts) do
local cd = p:get_header('Content-Disposition')
if (cd and cd:lower():match('^attachment')) then
return true
end
end
end
end,
description = 'Message contains attachments'
}

rspamd_config.MV_CASE = {
callback = function (task)
local mv = task:get_header('Mime-Version', true)
if (mv) then return true end
end,
description = 'Mime-Version .vs. MIME-Version',
score = 0.5
}

rspamd_config.FAKE_REPLY = {
callback = function (task)
local subject = task:get_header('Subject')
if (subject and subject:lower():find('^re:')) then
local ref = task:get_header('References')
local rt = task:get_header('In-Reply-To')
if (not (ref or rt)) then return true end
end
return false
end,
description = 'Fake reply',
score = 1.0
}

rspamd_config.CHECK_FROM = {
callback = function(task)
local envfrom = task:get_from(1)
local from = task:get_from(2)
if (from and from[1] and not from[1].name) then
task:insert_result('FROM_NO_DN', 1.0)
elseif (from and from[1] and from[1].name and
from[1].name:lower() == from[1].addr:lower()) then
task:insert_result('FROM_DN_EQ_ADDR', 1.0)
elseif (from and from[1] and from[1].name) then
task:insert_result('FROM_HAS_DN', 1.0)
-- Look for Mr/Mrs/Dr titles
local n = from[1].name:lower()
if (n:find('^mrs?[%.%s]') or n:find('^dr[%.%s]')) then
task:insert_result('FROM_NAME_HAS_TITLE', 1.0)
end
end
if (envfrom and from and envfrom[1] and from[1] and
envfrom[1].addr:lower() == from[1].addr:lower())
then
task:insert_result('FROM_EQ_ENVFROM', 1.0)
elseif (envfrom and envfrom[1] and envfrom[1].addr) then
task:insert_result('FROM_NEQ_ENVFROM', 1.0, from[1].addr, envfrom[1].addr)
end

local to = task:get_recipients(2)
if not (to and to[1]) then return false end
-- Check if FROM == TO
if (#to == 1 and to[1].addr:lower() == from[1].addr:lower()) then
task:insert_result('TO_EQ_FROM', 1.0)
elseif (#to == 1 and to[1].domain:lower() == from[1].domain:lower()) then
task:insert_result('TO_DOM_EQ_FROM_DOM', 1.0)
end
end
}

rspamd_config.FROM_NO_DN = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'From header does not have a display name',
score = 0.0
}

rspamd_config.FROM_DN_EQ_ADDR = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'From header display name is the same as the address',
score = 1.0
}

rspamd_config.FROM_HAS_DN = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'From header has a display name',
score = 0.0
}

rspamd_config.FROM_NAME_HAS_TITLE = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'From header display name has a title (Mr/Mrs/Dr)',
score = 1.0
}

rspamd_config.FROM_EQ_ENVFROM = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'From address is the same as the envelope',
score = 0.0
}

rspamd_config.FROM_NEQ_ENVFROM = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'From address is different to the envelope',
score = 0.0
}

rspamd_config.TO_EQ_FROM = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'To address matches the From address',
score = 0.0
}

rspamd_config.TO_DOM_EQ_FROM_DOM = {
callback = function()
-- Set by CHECK_FROM
end,
description = 'To domain is the same as the From domain',
score = 0.0
}


rspamd_config.CHECK_TO_CC = {
callback = function(task)
local rcpts = task:get_recipients(1)
local to = task:get_recipients(2)
local to_match_envrcpt = 0
if (not to) then return false end
-- Add symbol for recipient count
if (#to > 50) then
task:insert_result('RCPT_COUNT_GT_50', 1.0)
else
task:insert_result('RCPT_COUNT_' .. #to, 1.0)
end
-- Check for display names
local to_dn_count = 0
local to_dn_eq_addr_count = 0
for _, toa in ipairs(to) do
-- To: Recipients <noreply@dropbox.com>
if (toa['name'] and (toa['name']:lower() == 'recipient'
or toa['name']:lower() == 'recipients')) then
task:insert_result('TO_DN_RECIPIENTS', 1.0)
end
if (toa['name'] and toa['name']:lower() == toa['addr']:lower()) then
to_dn_eq_addr_count = to_dn_eq_addr_count + 1
elseif (toa['name']) then
to_dn_count = to_dn_count + 1
end
-- See if header recipients match envrcpts
if (rcpts) then
for _, rcpt in ipairs(rcpts) do
if (toa and toa['addr'] and rcpt and rcpt['addr'] and
rcpt['addr']:lower() == toa['addr']:lower())
then
to_match_envrcpt = to_match_envrcpt + 1
end
end
end
end
if (to_dn_count == 0 and to_dn_eq_addr_count == 0) then
task:insert_result('TO_DN_NONE', 1.0)
elseif (to_dn_count == #to) then
task:insert_result('TO_DN_ALL', 1.0)
elseif (to_dn_count > 0) then
task:insert_result('TO_DN_SOME', 1.0)
end
if (to_dn_eq_addr_count == #to) then
task:insert_result('TO_DN_EQ_ADDR_ALL', 1.0)
elseif (to_dn_eq_addr_count > 0) then
task:insert_result('TO_DN_EQ_ADDR_SOME', 1.0)
end

-- See if header recipients match envelope recipients
if (to_match_envrcpt == #to) then
task:insert_result('TO_MATCH_ENVRCPT_ALL', 1.0)
elseif (to_match_envrcpt > 0) then
task:insert_result('TO_MATCH_ENVRCPT_SOME', 1.0)
end
end
}

rspamd_config.TO_DN_RECIPIENTS = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'To header display name is "Recipients"',
score = 2.0
}

rspamd_config.TO_DN_NONE = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'None of the recipients have display names',
score = 0.0
}

rspamd_config.TO_DN_ALL = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'All of the recipients have display names',
score = 0.0
}

rspamd_config.TO_DN_SOME = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'Some of the recipients have display names',
score = 0.0
}

rspamd_config.TO_DN_EQ_ADDR_ALL = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'All of the recipients have display names that are the same as their address',
score = 0.0
}

rspamd_config.TO_DN_EQ_ADDR_SOME = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'Some of the recipients have display names that are the same as their address',
score = 0.0
}

rspamd_config.TO_MATCH_ENVRCPT_ALL = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'All of the recipients match the envelope',
score = 0.0
}

rspamd_config.TO_MATCH_ENVRCPT_SOME = {
callback = function()
-- Set by CHECK_TO_CC
end,
description = 'Some of the recipients match the envelope',
score = 0.0
}


rspamd_config.CHECK_MID = {
callback = function (task)
local mid = task:get_header('Message-ID')
if not mid then return false end
-- Check for 'bare' IP addresses in RHS
if mid:find("@%d+%.%d+%.%d+%.%d+>$") then
task:insert_result('MID_BARE_IP', 1.0)
end
-- Check for non-FQDN RHS
if mid:find("@[^%.]+>?$") then
task:insert_result('MID_RHS_NOT_FQDN', 1.0)
end
-- Check for missing <>'s
if not mid:find('^<[^>]+>$') then
task:insert_result('MID_MISSING_BRACKETS', 1.0)
end
-- Check for IP literal in RHS
if mid:find("@%[%d+%.%d+%.%d+%.%d+%]") then
task:insert_result('MID_RHS_IP_LITERAL', 1.0)
end
-- Check From address atrributes against MID
local from = task:get_from(2)
if (from and from[1] and from[1].domain) then
local fd = from[1].domain:lower()
local _,_,md = mid:find("@([^>]+)>?$")
-- See if all or part of the From address
-- can be found in the Message-ID
if (mid:lower():find(from[1].addr:lower(),1,true)) then
task:insert_result('MID_CONTAINS_FROM', 1.0)
elseif (md and fd == md:lower()) then
task:insert_result('MID_RHS_MATCH_FROM', 1.0)
end
end
end
}


rspamd_config.MID_BARE_IP = {
callback = function()
-- Set by CHECK_MID
end,
description = 'Message-ID RHS is a bare IP address',
score = 2.0
}

rspamd_config.MID_RHS_NOT_FQDN = {
callback = function()
-- Set by CHECK_MID
end,
description = 'Message-ID RHS is not a fully-qualified domain name',
score = 0.5
}

rspamd_config.MID_MISSING_BRACKETS = {
callback = function()
-- Set by CHECK_MID
end,
description = 'Message-ID is missing <>\'s',
score = 0.5
}

rspamd_config.MID_RHS_IP_LITERAL = {
callback = function ()
-- Set by CHECK_MID
end,
description = 'Message-ID RHS is an IP-literal',
score = 0.5
}

rspamd_config.MID_CONTAINS_FROM = {
callback = function ()
-- Set by CHECK_MID
end,
description = 'Message-ID contains From address',
score = 1.0
}

rspamd_config.MID_RHS_MATCH_FROM = {
callback = function ()
-- Set by CHECK_MID
end,
description = 'Message-ID RHS matches From domain',
score = 1.0
}

rspamd_config.CHECK_RECEIVED = {
callback = function (task)
local received = task:get_received_headers()
task:insert_result('RCVD_COUNT_' .. #received, 1.0)
end
}

rspamd_config.HAS_X_PRIO = {
callback = function (task)
local xprio = task:get_header('X-Priority');
if not xprio then return false end
local _,_,x = xprio:find('^%s?(%d+)');
if (x) then
task:insert_result('HAS_X_PRIO_' .. x, 1.0)
end
end
}

rspamd_config.CHECK_REPLYTO = {
callback = function (task)
local replyto = task:get_header('Reply-To')
if not replyto then return false end
local rt = util.parse_mail_address(replyto)
if not (rt and rt[1]) then
task:insert_result('REPLYTO_UNPARSEABLE', 1.0)
return false
else
task:insert_result('HAS_REPLYTO', 1.0)
end

-- See if Reply-To matches From in some way
local from = task:get_from(2)
local from_h = task:get_header('From')
if not (from and from[1]) then return false end
if (from_h and from_h == replyto) then
-- From and Reply-To are identical
task:insert_result('REPLYTO_EQ_FROM', 1.0)
else
if (from and from[1]) then
-- See if From and Reply-To addresses match
if (from[1].addr:lower() == rt[1].addr:lower()) then
task:insert_result('REPLYTO_ADDR_EQ_FROM', 1.0)
elseif (from[1].domain:lower() == rt[1].addr:lower()) then
task:insert_result('REPLYTO_DOM_EQ_FROM_DOM', 1.0)
elseif (from[1].domain:lower() ~= rt[1].domain:lower()) then
task:insert_result('REPLYTO_DOM_NEQ_FROM_DOM', 1.0)
end
-- See if the Display Names match
if (from[1].name and rt[1].name and from[1].name:lower() == rt[1].name:lower()) then
task:insert_result('REPLYTO_DN_EQ_FROM_DN', 1.0)
end
end
end
end
}

rspamd_config.REPLYTO_UNPARSEABLE = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Reply-To header could not be parsed',
score = 1.0
}

rspamd_config.HAS_REPLYTO = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Has Reply-To header',
score = 0.0
}

rspamd_config.REPLYTO_EQ_FROM = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Reply-To header is identical to From header',
score = 0.0
}

rspamd_config.REPLYTO_ADDR_EQ_FROM = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Reply-To address is the same as From',
score = 0.0
}

rspamd_config.REPLYTO_DOM_EQ_FROM_DOM = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Reply-To domain matches the From domain',
score = 0.0
}

rspamd_config.REPLYTO_DOM_NEQ_FROM_DOM = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Reply-To domain does not match the From domain',
score = 0.0
}

rspamd_config.REPLYTO_DN_EQ_FROM_DN = {
callback = function ()
-- Set by CHECK_REPLYTO
end,
description = 'Reply-To display name matches From',
score = 0.0
}

rspamd_config.CHECK_MIME = {
callback = function (task)
local parts = task:get_parts()
if not parts then return false end

-- Make sure there is a MIME-Version header
local mv = task:get_header('MIME-Version')
if (not mv) then
task:insert_result('MISSING_MIME_VERSION', 1.0)
end

local found_ma = false
local found_plain = false
local found_html = false

for _,p in ipairs(parts) do
local mtype,subtype = p:get_type()
local ctype = mtype:lower() .. '/' .. subtype:lower()
if (ctype == 'multipart/alternative') then
found_ma = true
end
if (ctype == 'text/plain') then
found_plain = true
end
if (ctype == 'text/html') then
found_html = true
end
end

if (found_ma) then
if (not found_plain) then
task:insert_result('MIME_MA_MISSING_TEXT', 1.0)
end
if (not found_html) then
task:insert_result('MIME_MA_MISSING_HTML', 1.0)
end
end
end
}

rspamd_config.MISSING_MIME_VERSION = {
callback = function ()
-- Set by CHECK_MIME
end,
description = 'MIME-Version header is missing',
score = 2.0
}

rspamd_config.MIME_MA_MISSING_TEXT = {
callback = function ()
-- Set by CHECK_MIME
end,
description = 'MIME multipart/alternative missing text/plain part',
score = 2.0
}

rspamd_config.MIME_NA_MISSING_HTML = {
callback = function ()
-- Set by CHECK_MIME
end,
description = 'MIME multipart/alternative missing text/html part',
score = 2.0
}

-- Used to be called IS_LIST
rspamd_config.PREVIOUSLY_DELIVERED = {
callback = function(task)
if not task:has_recipients(2) then return false end
local to = task:get_recipients(2)
local rcvds = task:get_header_full('Received')
if not rcvds then return false end
for _, rcvd in ipairs(rcvds) do
local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>")
if addr then
for _, toa in ipairs(to) do
if toa and toa.addr:lower() == addr then
return true, addr
end
end
return false
end
end
end,
description = 'Message either to a list or was forwarded',
score = 0.0
}


+ 91
- 0
rules/regexp/headers.lua View File

@@ -255,6 +255,22 @@ reconf['CC_EXCESS_QP'] = {
group = 'excessqp'
}

local subj_encoded_b64 = 'Subject=/\\=\\?\\S+\\?B\\?/iX'
local subj_needs_mime = 'Subject=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
reconf['SUBJ_EXCESS_BASE64'] = {
re = string.format('%s & !%s', subj_encoded_b64, subj_needs_mime),
score = 1.5,
description = 'Subject is unnecessarily encoded in base64',
group = 'excessb64'
}

local subj_encoded_qp = 'Subject=/\\=\\?\\S+\\?Q\\?/iX'
reconf['SUBJ_EXCESS_QP'] = {
re = string.format('%s & !%s', subj_encoded_qp, subj_needs_mime),
score = 1.2,
description = 'Subect is unnecessarily encoded in quoted-printable',
group = 'excessqp'
}

-- Detect forged outlook headers
-- OE X-Mailer header
@@ -803,3 +819,78 @@ reconf['GOOGLE_FORWARDING_MID_BROKEN'] = {
description = "Message had invalid Message-ID pre-forwarding",
group = 'header'
}

reconf['CTE_CASE'] = {
re = 'Content-Transfer-Encoding=/^[78]BsX',
description = '[78]Bit .vs. [78]bit',
score = 0.5,
group = header'
}

reconf['HAS_INTERSPIRE_SIG'] = {
re = string.format('((%s) & (%s) & (%s) & (%s)) | (%s)',
'header_exists(X-Mailer-LID)',
'header_exists(X-Mailer-RecptId)',
'header_exists(X-Mailer-SID)',
'header_exists(X-Mailer-Sent-By)',
'List-Unsubscribe=/\\/unsubscribe\\.php\\?M=[^&]+&C=[^&]+&L=[^&]+&N=[^>]+>$/Xi'),
description = "Has Interspire fingerprint",
score = 3.0,
group = 'header'
}

reconf['CT_EXTRA_SEMI'] = {
re = 'Content-Type=/;$/X',
description = 'Content-Type ends with a semi-colon',
score = 1.0,
group = 'header'
}

reconf['SUBJECT_ENDS_EXCLAIM'] = {
re = 'Subject=/!\\s*$/H',
description = 'Subject ends with an exclaimation',
score = 1.0,
group = 'headers'
}

reconf['SUBJECT_HAS_EXCLAIM'] = {
re = string.format('%s & !%s', 'Subject=/!/H', 'Subject=/!\\s*$/H'),
description = 'Subject contains an exclaimation',
score = 0.0,
group = 'headers'
}

reconf['SUBJECT_ENDS_QUESTION'] = {
re = 'Subject=/\\?\\s*$/H',
description = 'Subject ends with a question',
score = 1.0,
group = 'headers'
}

reconf['SUBJECT_HAS_QUESTION'] = {
re = string.format('%s & !%s', 'Subject=/\\?/H', 'Subject=/\\?\\s*$/H'),
description = 'Subject contains a question',
score = 0.0,
group = 'headers'
}

reconf['SUBJECT_HAS_CURRENCY'] = {
re = 'Subject=/$€$¢¥₽/H',
description = 'Subject contains currency',
score = 1.0,
group = 'headers'
}

reconf['SUBJECT_ENDS_SPACES'] = {
re = 'Subject=/\\s+$/H',
description = 'Subject ends with space characters',
score = 0.5,
group = 'headers'
}

reconf['HAS_ORG_HEADER'] = {
re = string.format('%s || %s', 'header_exists(Organization)', 'header_exists(Organisation)'),
description = 'Has Organization header',
score = 0.0,
group = 'headers'
}

+ 39
- 0
rules/regexp/misc.lua View File

@@ -0,0 +1,39 @@
--[[
Copyright (c) 2011-2016, Vsevolod Stakhov <vsevolod@highsecure.ru>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--


local reconf = config['regexp']

reconf['HTML_META_REFRESH_URL'] = {
-- Requires options { check_attachements = true; }
re = '/<meta\\s+http-equiv="refresh"\\s+content="\\d+;url=/{sa_raw_body}i',
description = "Has HTML Meta refresh URL",
score = 5.0
}

reconf['HAS_DATA_URI'] = {
-- Requires options { check_attachements = true; }
re = '/data:[^\\/]+\\/[^; ]+;base64,/{sa_raw_body}i',
description = "Has Data URI encoding"
}

reconf['DATA_URI_OBFU'] = {
-- Requires options { check_attachements = true; }
re = '/data:text\\/(?:plain|html);base64,/{sa_raw_body}i',
description = "Uses Data URI encoding to obfuscate plain or HTML in base64",
score = 2.0
}


+ 1
- 0
rules/rspamd.lua View File

@@ -25,6 +25,7 @@ dofile(local_rules .. '/regexp/headers.lua')
dofile(local_rules .. '/regexp/lotto.lua')
dofile(local_rules .. '/regexp/fraud.lua')
dofile(local_rules .. '/regexp/drugs.lua')
dofile(local_rules .. '/regexp/misc.lua')
dofile(local_rules .. '/regexp/upstream_spam_filters.lua')
dofile(local_rules .. '/regexp/compromised_hosts.lua')
dofile(local_rules .. '/html.lua')

Loading…
Cancel
Save