]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Fix] phishing: don't penalise same label under another TLD
authorVsevolod Stakhov <vsevolod@rspamd.com>
Wed, 8 Jul 2026 12:59:15 +0000 (13:59 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Wed, 8 Jul 2026 12:59:15 +0000 (13:59 +0100)
Three fixes for cross-TLD brand false positives
(e.g. brand.co.uk displayed over a brand.com href):

- Normalise the DMARC_POLICY_ALLOW domain to eSLD before comparing it
  with the link target tld: DMARC reports the raw From domain (possibly
  a subdomain), so the existing exclusion for authenticated mail never
  matched
- Compare registrable labels instead of stripping the last dot
  component: the old code only handled single-label suffixes, so
  brand.co.uk vs brand.com was levenshtein-compared as full strings and
  scored full weight; now the identical label yields weight 0
- Check strict_domains regardless of the computed weight (gated on
  tld ~= ptld, equivalent to the old reachable behaviour), so displaying
  a strictly protected domain over a same-label different-suffix target
  still fires at full weight

src/plugins/lua/phishing.lua

index 4dc3fd924ec805bc76c32c6270167f3df357bf9b..9dde4a87cbee7a176668f2662b7ebbaf2d3906d2 100644 (file)
@@ -225,8 +225,10 @@ local function phishing_cb(task)
   local dsym = task:get_symbol('DMARC_POLICY_ALLOW')
   if dsym then
     dsym = dsym[1] -- legacy stuff, need to take the first element
-    if dsym.options then
-      dmarc_dom = dsym.options[1]
+    if dsym.options and dsym.options[1] then
+      -- DMARC reports the raw From domain (possibly a subdomain),
+      -- normalise it to eSLD to allow comparison with url:get_tld()
+      dmarc_dom = util.get_tld(dsym.options[1])
     end
   end
 
@@ -301,19 +303,19 @@ local function phishing_cb(task)
           return
         end
 
-        -- Now we can safely remove the last dot component if it is the same
-        local b, _ = string.find(tld, '%.[^%.]+$')
-        local b1, _ = string.find(ptld, '%.[^%.]+$')
-
+        -- Compare merely the registrable labels: the first label of an eSLD is
+        -- the registrable label and the rest is the public suffix, which is not
+        -- meaningful for similarity (brand.co.uk vs brand.com is the same label
+        -- and commonly the same brand). Not applicable to numeric hosts and
+        -- hosts with unknown suffixes, where eSLD is the whole hostname.
         local stripped_tld, stripped_ptld = tld, ptld
-        if b1 and b then
-          if string.sub(tld, b) == string.sub(ptld, b1) then
-            stripped_ptld = string.gsub(ptld, '%.[^%.]+$', '')
-            stripped_tld = string.gsub(tld, '%.[^%.]+$', '')
-          end
+        local uflags, puflags = url:get_flags(), purl:get_flags()
+        if not (uflags.numeric or uflags.no_tld or puflags.numeric or puflags.no_tld) then
+          local lbl = string.match(tld, '^([^%.]+)%.')
+          local plbl = string.match(ptld, '^([^%.]+)%.')
 
-          if #ptld == 0 or #tld == 0 then
-            return false
+          if lbl and plbl then
+            stripped_tld, stripped_ptld = lbl, plbl
           end
         end
 
@@ -384,8 +386,14 @@ local function phishing_cb(task)
           end
         end
 
-        if weight > 0 then
+        if tld ~= ptld then
+          -- Check strict domains regardless of the computed weight: displaying
+          -- a strictly protected domain over a different link target is phishing
+          -- even when just the public suffix differs
           found_in_map(strict_domains_maps, purl, 1.0)
+        end
+
+        if weight > 0 then
           if not found_in_map(anchor_exceptions_maps) then
             if not found_in_map(phishing_exceptions_maps, purl, 1.0) then
               if domains then