numeric_ip {
enabled = true;
- # Scoring for different scenarios
- base_score = 1.5; # Basic numeric IP
- with_user_score = 4.0; # Numeric IP + user field
-
# Private IP ranges (10.x, 192.168.x, etc.)
allow_private_ranges = true;
- private_score = 0.5; # Lower score for private IPs
# OPTIONAL: Suspicious IP ranges map
# To enable, add in local.d/url_suspect.conf:
tld {
enabled = true;
- # Built-in suspicious TLDs (no map needed)
+ # Built-in suspicious TLDs
builtin_suspicious = [".tk", ".ml", ".ga", ".cf", ".gq"];
- builtin_score = 3.0;
-
- # Missing TLD score
- missing_tld_score = 2.0;
# OPTIONAL: Custom TLD map
# To enable, add in local.d/url_suspect.conf:
}
}
- # Symbol names (can be customized)
- symbols {
- # User/password symbols
- user_password = "URL_USER_PASSWORD";
- user_long = "URL_USER_LONG";
- user_very_long = "URL_USER_VERY_LONG";
-
- # Numeric IP symbols
- numeric_ip = "URL_NUMERIC_IP";
- numeric_ip_user = "URL_NUMERIC_IP_USER";
- numeric_private = "URL_NUMERIC_PRIVATE_IP";
-
- # TLD symbols
- no_tld = "URL_NO_TLD";
- suspicious_tld = "URL_SUSPICIOUS_TLD";
-
- # Unicode symbols
- bad_unicode = "URL_BAD_UNICODE";
- homograph = "URL_HOMOGRAPH_ATTACK";
- rtl_override = "URL_RTL_OVERRIDE";
- zero_width = "URL_ZERO_WIDTH_SPACES";
-
- # Structure symbols
- multiple_at = "URL_MULTIPLE_AT_SIGNS";
- backslash = "URL_BACKSLASH_PATH";
- excessive_dots = "URL_EXCESSIVE_DOTS";
- very_long = "URL_VERY_LONG";
- }
+
# ADVANCED: Global whitelist
# To enable, add in local.d/url_suspect.conf:
# EOD;
# }
- # Backward compatibility with R_SUSPICIOUS_URL
- # When enabled, R_SUSPICIOUS_URL symbol is inserted if any URL_* symbols fire
- compat_mode = true;
-
.include(try=true,priority=5) "${DBDIR}/dynamic/url_suspect.conf"
.include(try=true,priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/url_suspect.conf"
.include(try=true,priority=10) "$LOCAL_CONFDIR/override.d/url_suspect.conf"
local rspamd_util = require "rspamd_util"
local bit = require "bit"
+-- Symbol names (fixed, not configurable)
+local symbols = {
+ -- User/password symbols
+ user_password = "URL_USER_PASSWORD",
+ user_long = "URL_USER_LONG",
+ user_very_long = "URL_USER_VERY_LONG",
+ -- Numeric IP symbols
+ numeric_ip = "URL_NUMERIC_IP",
+ numeric_ip_user = "URL_NUMERIC_IP_USER",
+ numeric_private = "URL_NUMERIC_PRIVATE_IP",
+ -- TLD symbols
+ no_tld = "URL_NO_TLD",
+ suspicious_tld = "URL_SUSPICIOUS_TLD",
+ -- Unicode symbols
+ bad_unicode = "URL_BAD_UNICODE",
+ homograph = "URL_HOMOGRAPH_ATTACK",
+ rtl_override = "URL_RTL_OVERRIDE",
+ zero_width = "URL_ZERO_WIDTH_SPACES",
+ -- Structure symbols
+ multiple_at = "URL_MULTIPLE_AT_SIGNS",
+ backslash = "URL_BACKSLASH_PATH",
+ excessive_dots = "URL_EXCESSIVE_DOTS",
+ very_long = "URL_VERY_LONG"
+}
+
-- Default settings (work without any maps)
local settings = {
enabled = true,
suspicious = 64,
long = 128,
very_long = 256
- },
-
+ }
},
numeric_ip = {
enabled = true,
- base_score = 1.5,
- with_user_score = 4.0,
- allow_private_ranges = true,
- private_score = 0.5
+ allow_private_ranges = true
},
tld = {
enabled = true,
- builtin_suspicious = { ".tk", ".ml", ".ga", ".cf", ".gq" },
- builtin_score = 3.0,
- missing_tld_score = 2.0
+ builtin_suspicious = { ".tk", ".ml", ".ga", ".cf", ".gq" }
},
unicode = {
enabled = true,
max_url_length = 2048
}
},
- symbols = {
- -- User/password symbols
- user_password = "URL_USER_PASSWORD",
- user_long = "URL_USER_LONG",
- user_very_long = "URL_USER_VERY_LONG",
- -- Numeric IP symbols
- numeric_ip = "URL_NUMERIC_IP",
- numeric_ip_user = "URL_NUMERIC_IP_USER",
- numeric_private = "URL_NUMERIC_PRIVATE_IP",
- -- TLD symbols
- no_tld = "URL_NO_TLD",
- suspicious_tld = "URL_SUSPICIOUS_TLD",
- -- Unicode symbols
- bad_unicode = "URL_BAD_UNICODE",
- homograph = "URL_HOMOGRAPH_ATTACK",
- rtl_override = "URL_RTL_OVERRIDE",
- zero_width = "URL_ZERO_WIDTH_SPACES",
- -- Structure symbols
- multiple_at = "URL_MULTIPLE_AT_SIGNS",
- backslash = "URL_BACKSLASH_PATH",
- excessive_dots = "URL_EXCESSIVE_DOTS",
- very_long = "URL_VERY_LONG"
- },
use_whitelist = false,
- custom_checks = {},
- compat_mode = true
+ custom_checks = {}
}
-- Optional maps (only loaded if enabled)
lua_util.debugm(N, task, "Checking user field length: %d chars", user_len)
- -- Length-based scoring (built-in, no map needed)
+ -- Length-based detection
if user_len > cfg.length_thresholds.very_long then
table.insert(findings, {
- symbol = settings.symbols.user_very_long,
- score = 5.0,
+ symbol = symbols.user_very_long,
options = { string.format("%d", user_len) }
})
elseif user_len > cfg.length_thresholds.long then
table.insert(findings, {
- symbol = settings.symbols.user_long,
- score = 3.0,
+ symbol = symbols.user_long,
options = { string.format("%d", user_len) }
})
elseif user_len > cfg.length_thresholds.suspicious then
table.insert(findings, {
- symbol = settings.symbols.user_password,
- score = 2.0,
+ symbol = symbols.user_password,
options = { host or "unknown" }
})
else
-- Normal length user
table.insert(findings, {
- symbol = settings.symbols.user_password,
- score = 2.0,
+ symbol = symbols.user_password,
options = { host or "unknown" }
})
end
if is_private and cfg.allow_private_ranges then
table.insert(findings, {
- symbol = settings.symbols.numeric_private,
- score = cfg.private_score,
+ symbol = symbols.numeric_private,
options = { host }
})
else
-- Check if user present (more suspicious)
if bit.band(flags, url_flags_tab.has_user) ~= 0 then
table.insert(findings, {
- symbol = settings.symbols.numeric_ip_user,
- score = cfg.with_user_score,
+ symbol = symbols.numeric_ip_user,
options = { host }
})
else
table.insert(findings, {
- symbol = settings.symbols.numeric_ip,
- score = cfg.base_score,
+ symbol = symbols.numeric_ip,
options = { host }
})
end
if bit.band(flags, url_flags_tab.numeric) == 0 then
lua_util.debugm(N, task, "URL has no TLD: %s", host)
table.insert(findings, {
- symbol = settings.symbols.no_tld,
- score = cfg.missing_tld_score,
+ symbol = symbols.no_tld,
options = { host }
})
end
return findings
end
- -- Check built-in suspicious TLDs (no map needed)
+ -- Check built-in suspicious TLDs
for _, suspicious_tld in ipairs(cfg.builtin_suspicious) do
if tld == suspicious_tld or tld:sub(-#suspicious_tld) == suspicious_tld then
lua_util.debugm(N, task, "URL uses suspicious TLD: %s", tld)
table.insert(findings, {
- symbol = settings.symbols.suspicious_tld,
- score = cfg.builtin_score,
+ symbol = symbols.suspicious_tld,
options = { tld }
})
break
if cfg.check_validity and not rspamd_util.is_valid_utf8(url_text) then
lua_util.debugm(N, task, "URL has invalid UTF-8")
table.insert(findings, {
- symbol = settings.symbols.bad_unicode,
- score = 3.0,
+ symbol = symbols.bad_unicode,
options = { host or "unknown" }
})
end
if cfg.check_zero_width and bit.band(flags, url_flags_tab.zw_spaces) ~= 0 then
lua_util.debugm(N, task, "URL contains zero-width spaces")
table.insert(findings, {
- symbol = settings.symbols.zero_width,
- score = 7.0,
+ symbol = symbols.zero_width,
options = { host or "unknown" }
})
end
if rspamd_util.is_utf_spoofed(host) then
lua_util.debugm(N, task, "URL uses homograph attack: %s", host)
table.insert(findings, {
- symbol = settings.symbols.homograph,
- score = 5.0,
+ symbol = symbols.homograph,
options = { host }
})
end
if cfg.check_rtl_override and url_text:find("\226\128\174") then
lua_util.debugm(N, task, "URL contains RTL override")
table.insert(findings, {
- symbol = settings.symbols.rtl_override,
- score = 6.0,
+ symbol = symbols.rtl_override,
options = { host or "unknown" }
})
end
if at_count > cfg.max_at_signs then
lua_util.debugm(N, task, "URL has %d @ signs", at_count)
table.insert(findings, {
- symbol = settings.symbols.multiple_at,
- score = 3.0,
+ symbol = symbols.multiple_at,
options = { string.format("%d", at_count) }
})
end
if bit.band(flags, url_flags_tab.obscured) ~= 0 and url_text:find("\\") then
lua_util.debugm(N, task, "URL contains backslashes")
table.insert(findings, {
- symbol = settings.symbols.backslash,
- score = 2.0,
+ symbol = symbols.backslash,
options = { host or "unknown" }
})
end
if dot_count > cfg.max_host_dots then
lua_util.debugm(N, task, "URL hostname has %d dots", dot_count)
table.insert(findings, {
- symbol = settings.symbols.excessive_dots,
- score = 2.0,
+ symbol = symbols.excessive_dots,
options = { string.format("%d", dot_count) }
})
end
if cfg.check_length and #url_text > cfg.max_url_length then
lua_util.debugm(N, task, "URL is very long: %d chars", #url_text)
table.insert(findings, {
- symbol = settings.symbols.very_long,
- score = 1.5,
+ symbol = symbols.very_long,
options = { string.format("%d", #url_text) }
})
end
-- TLD and structure checks don't have corresponding URL flags, so need all URLs
local need_all_urls = (
(settings.checks.tld and settings.checks.tld.enabled) or
- (settings.checks.structure and settings.checks.structure.enabled and
- (settings.checks.structure.check_multiple_at or
- settings.checks.structure.check_excessive_dots or
- settings.checks.structure.check_length))
+ (settings.checks.structure and settings.checks.structure.enabled and
+ (settings.checks.structure.check_multiple_at or
+ settings.checks.structure.check_excessive_dots or
+ settings.checks.structure.check_length))
)
if need_all_urls then
return false
end
- local total_findings = 0
-
for _, url in ipairs(suspect_urls) do
local url_findings = analyze_url(task, url, settings)
for _, finding in ipairs(url_findings) do
- task:insert_result(finding.symbol, finding.score, finding.options or {})
- total_findings = total_findings + 1
- end
- end
-
- -- Backward compatibility: R_SUSPICIOUS_URL
- if settings.compat_mode and total_findings > 0 then
- -- Check if we inserted any symbols
- local has_findings = false
- for _, symbol_name in pairs(settings.symbols) do
- if task:has_symbol(symbol_name) then
- has_findings = true
- break
- end
- end
-
- if has_findings then
- task:insert_result('R_SUSPICIOUS_URL', 5.0)
+ task:insert_result(finding.symbol, 1.0, finding.options or {})
end
end
})
-- Register all symbol names as virtual
- for _, symbol_name in pairs(settings.symbols) do
+ for _, symbol_name in pairs(symbols) do
rspamd_config:register_symbol({
name = symbol_name,
type = 'virtual',
group = 'url'
})
end
-
- -- Backward compat symbol
- if settings.compat_mode then
- rspamd_config:register_symbol({
- name = 'R_SUSPICIOUS_URL',
- type = 'virtual',
- parent = id,
- score = 5.0,
- group = 'url',
- description = 'Suspicious URL (legacy symbol)'
- })
- end
end