From: Steve Freegard Date: Fri, 22 Apr 2016 11:20:21 +0000 (+0100) Subject: New rules X-Git-Tag: 1.3.0~669^2 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e36bf64097a27ee6d0595b6923f9a811d49ddb09;p=rspamd.git New rules --- diff --git a/conf/composites.conf b/conf/composites.conf index bfa0b1b47..b3372a997 100644 --- a/conf/composites.conf +++ b/conf/composites.conf @@ -22,6 +22,14 @@ composite { name = "FORGED_SENDER_MAILLIST"; expression = "FORGED_SENDER & -MAILLIST"; } +composite { + name = "FORGED_SENDER_FORWARDING"; + expression = "FORGED_SENDER & g:forwarding"; +} +composite { + name = "FORGED_SENDER_VERP_SRS"; + expression = "FORGED_SENDER & (ENVFROM_PRVS | ENVFROM_VERP)"; +} composite { name = "FORGED_MUA_MAILLIST"; expression = "g:mua and -MAILLIST"; @@ -32,4 +40,4 @@ composite { } .include(try=true; priority=1) "$LOCAL_CONFDIR/local.d/composites.conf" -.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/composites.conf" \ No newline at end of file +.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/composites.conf" diff --git a/rules/forwarding.lua b/rules/forwarding.lua new file mode 100644 index 000000000..6ee0b9a97 --- /dev/null +++ b/rules/forwarding.lua @@ -0,0 +1,109 @@ +--[[ +Copyright (c) 2011-2016, Vsevolod Stakhov + +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. +]]-- + +-- Rules to detect forwarding + +rspamd_config.FWD_GOOGLE = { + callback = function (task) + if not (task:has_from(1) and task:has_recipients(1)) then + return false + end + local envfrom = task:get_from(1) + local envrcpts = task:get_recipients(1) + -- Forwarding will only be to a single recipient + if table.getn(envrcpts) > 1 then return false end + -- Get recipient and compute VERP address + local rcpt = envrcpts[1].addr:lower() + local verp = rcpt:gsub('@','=') + -- Get the user portion of the envfrom + local ef_user = envfrom[1].user:lower() + -- Check for a match + if ef_user:find('+caf_=' .. verp, 1, true) then + local _,_,user = ef_user:find('^(.+)+caf_=') + if user then + user = user .. '@' .. envfrom[1].domain + return true, user + end + end + return false + end, + score = 0.1, + description = "Message was forwarded by Google", + group = "forwarding" +} + +rspamd_config.FWD_SRS = { + callback = function (task) + if not (task:has_from(1) and task:has_recipients(1)) then + return false + end + local envfrom = task:get_from(1) + local envrcpts = task:get_recipients(1) + -- Forwarding is only to a single recipient + if table.getn(envrcpts) > 1 then return false end + -- Get recipient and compute rewritten SRS address + local srs = '=' .. envrcpts[1].domain:lower() .. + '=' .. envrcpts[1].user:lower() + if envfrom[1].user:lower():find('^srs[01]=') and + envfrom[1].user:lower():find(srs, 1, false) + then + return true + end + return false + end, + score = 0.1, + description = "Message was forwarded using SRS", + group = "forwarding" +} + +rspamd_config.FORWARDED = { + callback = function (task) + if not task:has_recipients(1) then return false end + local envrcpts = task:get_recipients(1) + -- Forwarding will only be for single recipient messages + if table.getn(envrcpts) > 1 then return false end + -- Get any other headers we might need + local lu = task:get_header('List-Unsubscribe') + local to = task:get_recipients(2) + local matches = 0 + -- Retrieve and loop through all Received headers + local rcvds = task:get_header_full('Received') + for _, rcvd in ipairs(rcvds) do + local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>") + if addr then + matches = matches + 1 + -- Check that it doesn't match the envrcpt + -- TODO: remove any plus addressing? + if addr ~= envrcpts[1].addr:lower() then + -- Check for mailing-lists as they will have the same signature + if matches < 2 and lu and to and to[1].addr:lower() == addr then + return false + else + return true, addr + end + end + -- Prevent any other iterations as we only want + -- process the first matching Received header + return false + end + end + return false + end, + score = 0.1, + description = "Message was forwarded", + group = "forwarding" +} + diff --git a/rules/misc.lua b/rules/misc.lua index b3926e46b..f72efea87 100644 --- a/rules/misc.lua +++ b/rules/misc.lua @@ -237,3 +237,77 @@ rspamd_config.MULTIPLE_UNIQUE_HEADERS = { group = 'headers', description = 'Repeated unique headers' } + +rspamd_config.ENVFROM_PRVS = { + callback = function (task) + -- Detect PRVS/BATV addresses to avoid FORGED_SENDER + -- https://en.wikipedia.org/wiki/Bounce_Address_Tag_Validation + if not (task:has_from(1) and task:has_from(2)) then + return false + end + local envfrom = task:get_from(1) + local tag,ef = envfrom[1].addr:lower():match("^prvs=([^=]+)=(.+)$") + if not ef then return false end + -- See if it matches the From header + local from = task:get_from(2) + if ef == from[1].addr:lower() then + return true + end + return false + end, + score = 0.01, + description = "Envelope From is a PRVS address that matches the From address", + group = 'prvs' +} + +rspamd_config.ENVFROM_VERP = { + callback = function (task) + if not (task:has_from(1) and task:has_recipients(1)) then + return false + end + local envfrom = task:get_from(1) + local envrcpts = task:get_recipients(1) + -- VERP only works for single recipient messages + if table.getn(envrcpts) > 1 then return false end + -- Get recipient and compute VERP address + local rcpt = envrcpts[1].addr:lower() + local verp = rcpt:gsub('@','=') + -- Get the user portion of the envfrom + local ef_user = envfrom[1].user:lower() + -- See if the VERP representation of the recipient appears in it + if ef_user:find(verp, 1, true) + and not ef_user:find('+caf_=' .. verp, 1, true) -- Google Forwarding + and not ef_user:find('^srs[01]=') -- SRS + then + return true + end + return false + end, + score = 0.01, + description = "Envelope From is a VERP address", + group = "mailing_list" +} + +rspamd_config.RCVD_TLS_ALL = { + callback = function (task) + local rcvds = task:get_header_full('Received') + local count = 0 + local encrypted = 0 + for _, rcvd in ipairs(rcvds) do + count = count + 1 + local r = rcvd['decoded']:lower() + local by = r:match('^by%s+([^%s]+)') or r:match('%sby%s+([^%s]+)') + local with = r:match('%swith%s+(e?smtps?a?)') + if with and with:match('esmtps') then + encrypted = encrypted + 1 + end + end + if (count > 0 and count == encrypted) then + return true + end + end, + score = 0.01, + description = "All hops used encrypted transports", + group = "encryption" +} + diff --git a/rules/regexp/upstream_spam_filters.lua b/rules/regexp/upstream_spam_filters.lua new file mode 100644 index 000000000..066b8fd57 --- /dev/null +++ b/rules/regexp/upstream_spam_filters.lua @@ -0,0 +1,46 @@ +--[[ +Copyright (c) 2011-2016, Vsevolod Stakhov + +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. +]]-- + +-- Rules for upstream services that have already run spam checks + +reconf['PRECEDENCE_BULK'] = { + re = 'Precedence=/bulk/Hi', + score = 3, + description = "Message marked as bulk", + group = 'upstream_spam_filters' +} + +reconf['MICROSOFT_SPAM'] = { + -- https://technet.microsoft.com/en-us/library/dn205071(v=exchg.150).aspx + re = 'X-Forefront-Antispam-Report=/SFV:SPM/H', + score = 10, + description = "Microsoft says the messge is spam", + group = 'upstream_spam_filters' +} + +reconf['AOL_SPAM'] = { + re = 'X-AOL-Global-Disposition=/^S/H', + score = 5, + description = "AOL says this message is spam", + group = 'upstream_spam_filters' +} + +reconf['SPAM_FLAG'] = { + re = 'X-Spam-Flag=/^(?:yes|true)/Hi', + score = 5, + description = "Message was already marked as spam", + group = 'upstream_spam_filters' +} diff --git a/rules/rspamd.lua b/rules/rspamd.lua index 4c6ce61d9..a3cdc919b 100644 --- a/rules/rspamd.lua +++ b/rules/rspamd.lua @@ -28,9 +28,11 @@ 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/upstream_spam_filters.lua') dofile(local_rules .. '/html.lua') dofile(local_rules .. '/misc.lua') dofile(local_rules .. '/http_headers.lua') +dofile(local_rules .. '/forwarding.lua') local function file_exists(filename) local file = io.open(filename)