You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

misc.lua 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. --[[
  2. Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. -- This is main lua config file for rspamd
  14. local util = require "rspamd_util"
  15. local rspamd_regexp = require "rspamd_regexp"
  16. local rspamd_logger = require "rspamd_logger"
  17. local reconf = config['regexp']
  18. -- Uncategorized rules
  19. local subject_re = rspamd_regexp.create('/^(?:(?:Re|Fwd|Fw|Aw|Antwort|Sv):\\s*)+(.+)$/i')
  20. -- Local rules
  21. local r_bgcolor = '/BGCOLOR=/iP'
  22. local r_font_color = '/font color=[\\"\']?\\#FFFFFF[\\"\']?/iP'
  23. reconf['R_WHITE_ON_WHITE'] = string.format('(!(%s) & (%s))', r_bgcolor, r_font_color)
  24. reconf['R_FLASH_REDIR_IMGSHACK'] = '/^(?:http:\\/\\/)?img\\d{1,5}\\.imageshack\\.us\\/\\S+\\.swf/U'
  25. -- Local functions
  26. -- Subject issues
  27. local function test_subject(task, check_function, rate)
  28. local function normalize_linear(a, x)
  29. local f = a * x
  30. return true, (( f < 1 ) and f or 1), tostring(x)
  31. end
  32. local sbj = task:get_header('Subject')
  33. if sbj then
  34. local stripped_subject = subject_re:search(sbj, false, true)
  35. if stripped_subject and stripped_subject[1] and stripped_subject[1][2] then
  36. sbj = stripped_subject[1][2]
  37. end
  38. local l = util.strlen_utf8(sbj)
  39. if check_function(sbj, l) then
  40. return normalize_linear(rate, l)
  41. end
  42. end
  43. return false
  44. end
  45. rspamd_config.SUBJ_ALL_CAPS = {
  46. callback = function(task)
  47. local caps_test = function(sbj, len)
  48. return util.is_uppercase(sbj)
  49. end
  50. return test_subject(task, caps_test, 1.0/40.0)
  51. end,
  52. score = 3.0,
  53. group = 'subject',
  54. description = 'All capital letters in subject'
  55. }
  56. rspamd_config.LONG_SUBJ = {
  57. callback = function(task)
  58. local length_test = function(sbj, len)
  59. return len > 200
  60. end
  61. return test_subject(task, length_test, 1.0/400.0)
  62. end,
  63. score = 3.0,
  64. group = 'subject',
  65. description = 'Subject is too long'
  66. }
  67. -- Different text parts
  68. rspamd_config.R_PARTS_DIFFER = function(task)
  69. local distance = task:get_mempool():get_variable('parts_distance', 'double')
  70. if distance then
  71. local nd = tonumber(distance)
  72. -- ND is relation of different words to total words
  73. if nd >= 0.5 then
  74. local tw = task:get_mempool():get_variable('total_words', 'int')
  75. if tw then
  76. local score
  77. if tw > 30 then
  78. -- We are confident about difference
  79. score = (nd - 0.5) * 2.0
  80. else
  81. -- We are not so confident about difference
  82. score = (nd - 0.5)
  83. end
  84. task:insert_result('R_PARTS_DIFFER', score,
  85. string.format('%.1f%%', tostring(100.0 * nd)))
  86. end
  87. end
  88. end
  89. return false
  90. end
  91. -- Date issues
  92. rspamd_config.MISSING_DATE = function(task)
  93. if rspamd_config:get_api_version() >= 5 then
  94. local date = task:get_header_raw('Date')
  95. if date == nil or date == '' then
  96. return true
  97. end
  98. end
  99. return false
  100. end
  101. rspamd_config.DATE_IN_FUTURE = function(task)
  102. if rspamd_config:get_api_version() >= 5 then
  103. local dm = task:get_date{format = 'message'}
  104. local dt = task:get_date{format = 'connect'}
  105. -- An 2 hour
  106. if dm > 0 and dm - dt > 7200 then
  107. return true
  108. end
  109. end
  110. return false
  111. end
  112. rspamd_config.DATE_IN_PAST = function(task)
  113. if rspamd_config:get_api_version() >= 5 then
  114. local dm = task:get_date{format = 'message', gmt = true}
  115. local dt = task:get_date{format = 'connect', gmt = true}
  116. -- A day
  117. if dm > 0 and dt - dm > 86400 then
  118. return true
  119. end
  120. end
  121. return false
  122. end
  123. rspamd_config.R_SUSPICIOUS_URL = function(task)
  124. local urls = task:get_urls()
  125. if urls then
  126. for i,u in ipairs(urls) do
  127. if u:is_obscured() then
  128. task:insert_result('R_SUSPICIOUS_URL', 1.0, u:get_host())
  129. end
  130. end
  131. end
  132. return false
  133. end
  134. rspamd_config.BROKEN_HEADERS = {
  135. callback = function(task)
  136. if task:has_flag('broken_headers') then
  137. return true
  138. end
  139. return false
  140. end,
  141. score = 1.0,
  142. group = 'header',
  143. description = 'Headers structure is likely broken'
  144. }
  145. rspamd_config.HEADER_RCONFIRM_MISMATCH = {
  146. callback = function (task)
  147. local header_from = nil
  148. local cread = task:get_header('X-Confirm-Reading-To')
  149. if task:has_from('mime') then
  150. header_from = task:get_from('mime')[1]
  151. end
  152. local header_cread = nil
  153. if cread then
  154. local headers_cread = util.parse_mail_address(cread)
  155. if headers_cread then header_cread = headers_cread[1] end
  156. end
  157. if header_from and header_cread then
  158. if not string.find(header_from['addr'], header_cread['addr']) then
  159. return true
  160. end
  161. end
  162. return false
  163. end,
  164. score = 2.0,
  165. group = 'header',
  166. description = 'Read confirmation address is different to from address'
  167. }
  168. rspamd_config.HEADER_FORGED_MDN = {
  169. callback = function (task)
  170. local mdn = task:get_header('Disposition-Notification-To')
  171. local header_rp = nil
  172. if task:has_from('smtp') then
  173. header_rp = task:get_from('smtp')[1]
  174. end
  175. -- Parse mail addr
  176. local header_mdn = nil
  177. if mdn then
  178. local headers_mdn = util.parse_mail_address(mdn)
  179. if headers_mdn then header_mdn = headers_mdn[1] end
  180. end
  181. if header_mdn and not header_rp then return true end
  182. if header_rp and not header_mdn then return false end
  183. if header_mdn and header_mdn['addr'] ~= header_rp['addr'] then
  184. return true
  185. end
  186. return false
  187. end,
  188. score = 2.0,
  189. group = 'header',
  190. description = 'Read confirmation address is different to return path'
  191. }
  192. local headers_unique = {
  193. 'Content-Type',
  194. 'Content-Transfer-Encoding',
  195. -- https://tools.ietf.org/html/rfc5322#section-3.6
  196. 'Date',
  197. 'From',
  198. 'Sender',
  199. 'Reply-To',
  200. 'To',
  201. 'Cc',
  202. 'Bcc',
  203. 'Message-ID',
  204. 'In-Reply-To',
  205. 'References',
  206. 'Subject'
  207. }
  208. rspamd_config.MULTIPLE_UNIQUE_HEADERS = {
  209. callback = function (task)
  210. local res = 0
  211. local res_tbl = {}
  212. for i,hdr in ipairs(headers_unique) do
  213. local h = task:get_header_full(hdr)
  214. if h and #h > 1 then
  215. res = res + 1
  216. table.insert(res_tbl, hdr)
  217. end
  218. end
  219. if res > 0 then
  220. return true,res,table.concat(res_tbl, ',')
  221. end
  222. return false
  223. end,
  224. score = 5.0,
  225. group = 'header',
  226. description = 'Repeated unique headers'
  227. }
  228. rspamd_config.ENVFROM_PRVS = {
  229. callback = function (task)
  230. -- Detect PRVS/BATV addresses to avoid FORGED_SENDER
  231. -- https://en.wikipedia.org/wiki/Bounce_Address_Tag_Validation
  232. if not (task:has_from(1) and task:has_from(2)) then
  233. return false
  234. end
  235. local envfrom = task:get_from(1)
  236. local tag,ef = envfrom[1].addr:lower():match("^prvs=([^=]+)=(.+)$")
  237. if not ef then return false end
  238. -- See if it matches the From header
  239. local from = task:get_from(2)
  240. if ef == from[1].addr:lower() then
  241. return true
  242. end
  243. return false
  244. end,
  245. score = 0.01,
  246. description = "Envelope From is a PRVS address that matches the From address",
  247. group = 'prvs'
  248. }
  249. rspamd_config.ENVFROM_VERP = {
  250. callback = function (task)
  251. if not (task:has_from(1) and task:has_recipients(1)) then
  252. return false
  253. end
  254. local envfrom = task:get_from(1)
  255. local envrcpts = task:get_recipients(1)
  256. -- VERP only works for single recipient messages
  257. if table.getn(envrcpts) > 1 then return false end
  258. -- Get recipient and compute VERP address
  259. local rcpt = envrcpts[1].addr:lower()
  260. local verp = rcpt:gsub('@','=')
  261. -- Get the user portion of the envfrom
  262. local ef_user = envfrom[1].user:lower()
  263. -- See if the VERP representation of the recipient appears in it
  264. if ef_user:find(verp, 1, true)
  265. and not ef_user:find('+caf_=' .. verp, 1, true) -- Google Forwarding
  266. and not ef_user:find('^srs[01]=') -- SRS
  267. then
  268. return true
  269. end
  270. return false
  271. end,
  272. score = 0.01,
  273. description = "Envelope From is a VERP address",
  274. group = "mailing_list"
  275. }
  276. rspamd_config.RCVD_TLS_ALL = {
  277. callback = function (task)
  278. local rcvds = task:get_header_full('Received')
  279. if not rcvds then return false end
  280. local count = 0
  281. local encrypted = 0
  282. for _, rcvd in ipairs(rcvds) do
  283. count = count + 1
  284. local r = rcvd['decoded']:lower()
  285. local by = r:match('^by%s+([^%s]+)') or r:match('%sby%s+([^%s]+)')
  286. local with = r:match('%swith%s+(e?smtps?a?)')
  287. if with and with:match('esmtps') then
  288. encrypted = encrypted + 1
  289. end
  290. end
  291. if (count > 0 and count == encrypted) then
  292. return true
  293. end
  294. end,
  295. score = 0.01,
  296. description = "All hops used encrypted transports",
  297. group = "encryption"
  298. }
  299. rspamd_config.MISSING_FROM = {
  300. callback = function(task)
  301. local from = task:get_header('From')
  302. if from == nil or from == '' then
  303. return true
  304. end
  305. return false
  306. end,
  307. score = 2.0,
  308. group = 'header',
  309. description = 'Missing From: header'
  310. }