← Back
Editing: fuzzy_html_phishing.lua
--[[ Copyright (c) 2025, Vsevolod Stakhov <vsevolod@rspamd.com> 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 lua_util = require "lua_util" local N = 'fuzzy_html_phishing' -- Collect symbols from fuzzy rules that have html_shingles enabled local html_fuzzy_symbols = {} local fuzzy_conf = rspamd_config:get_all_opt('fuzzy_check') if fuzzy_conf and fuzzy_conf.rule then local function process_rule(rule) if not rule.html_shingles then return end -- Default symbol for the rule if rule.symbol then html_fuzzy_symbols[rule.symbol] = true end -- Per-flag mapped symbols if rule.fuzzy_map then for _, map in pairs(rule.fuzzy_map) do if type(map) == 'table' and map.symbol then html_fuzzy_symbols[map.symbol] = true end end end end for _, rule in pairs(fuzzy_conf.rule) do if type(rule) == 'table' then if rule.servers or rule.read_servers or rule.write_servers then -- Unnamed rule process_rule(rule) else -- Named rules container for _, subrule in pairs(rule) do if type(subrule) == 'table' then process_rule(subrule) end end end end end end if not next(html_fuzzy_symbols) then lua_util.debugm(N, rspamd_config, 'no fuzzy rules with html_shingles enabled, skip registration') return end local function check_fuzzy_mismatch(task) local text_parts = task:get_text_parts() if not text_parts then return end local has_html = false for _, tp in ipairs(text_parts) do if tp:is_html() then has_html = true break end end if not has_html then return end local all_symbols = task:get_symbols_all() if not all_symbols then return end for _, sym in ipairs(all_symbols) do if not html_fuzzy_symbols[sym.name] or not sym.options then goto continue end local matched = {} for _, opt in ipairs(sym.options) do local mtype = opt:match('^%d+:%w+:[%d%.]+:(%a+)') if mtype then matched[mtype] = true end end if matched['txt'] and not matched['html'] then task:insert_result('FUZZY_TEXT_WITHOUT_HTML', 1.0, sym.name) lua_util.debugm(N, task, 'text matched but html did not for %s', sym.name) elseif matched['html'] and not matched['txt'] then task:insert_result('FUZZY_HTML_WITHOUT_TEXT', 1.0, sym.name) lua_util.debugm(N, task, 'html matched but text did not for %s', sym.name) end -- Phishing detection: HTML template matches but domains differ if matched['html'] and not matched['htmld'] then task:insert_result('FUZZY_HTML_PHISHING', 1.0, sym.name) lua_util.debugm(N, task, 'html template matched but domains differ for %s (possible phishing)', sym.name) end ::continue:: end end local cb_id = rspamd_config:register_symbol{ name = 'FUZZY_MISMATCH_CHECK', type = 'callback', callback = check_fuzzy_mismatch, score = 0.0, group = 'fuzzy', description = 'Check for text/HTML fuzzy type mismatches', } rspamd_config:register_symbol{ name = 'FUZZY_TEXT_WITHOUT_HTML', type = 'virtual', score = 4.0, parent = cb_id, group = 'fuzzy', description = 'Text fuzzy matches but HTML structure does not (possible template swap)', } rspamd_config:register_symbol{ name = 'FUZZY_HTML_WITHOUT_TEXT', type = 'virtual', score = 2.0, parent = cb_id, group = 'fuzzy', description = 'HTML structure fuzzy matches but text content does not', } rspamd_config:register_symbol{ name = 'FUZZY_HTML_PHISHING', type = 'virtual', score = 6.0, parent = cb_id, group = 'fuzzy', description = 'HTML template matches but link domains differ (possible phishing)', } rspamd_config:register_dependency('FUZZY_MISMATCH_CHECK', 'FUZZY_CALLBACK')
Save File
Cancel