]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
Rules updates
authorSteve Freegard <steve@stevefreegard.com>
Mon, 21 Nov 2016 12:55:14 +0000 (12:55 +0000)
committerSteve Freegard <steve@stevefreegard.com>
Mon, 21 Nov 2016 12:55:14 +0000 (12:55 +0000)
rules/misc.lua
rules/regexp/headers.lua
rules/regexp/misc.lua [new file with mode: 0644]
rules/rspamd.lua

index 89ba97fb1ffcd265bb4de2e9ddbab2a5915764c8..9e30fe23f21066dcdb4223f40d1154b0ea7dbb8f 100644 (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
+}
+
index ef0adc6b1a6ad7fc1b048d810138dad3f470e390..e5bce8cea0f2bdbe11453dabd4d12fb6d595d4c0 100644 (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'
+}
diff --git a/rules/regexp/misc.lua b/rules/regexp/misc.lua
new file mode 100644 (file)
index 0000000..a819ec7
--- /dev/null
@@ -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
+}
+
index f5a5ed14ec52fc7182e5a64942f3cb2cce9aac46..b02e6b1afe6fd44f0102001786ca1730b3402071 100644 (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')