java-topology/defects/mastodon/unit/test_mastodon_cwe1333.rb
russell@unturf.com c24246e2e2 feat: add 5 outreach docs (33 defects) + mastodon CWE-1333 benchmark
Outreach docs (unblock intel page generation):
- kdenlive: 10 defects (8 CWE-407 + 1 CWE-362 + 1 keyframe), C++
- libreoffice: 5 defects (Writer, Calc, SFX, Impress), C++
- maven: 7 defects (graph, lifecycle, sort-by-indexOf), Java
- cpython: 7 defects (pkgutil, codegen, mock, pmerge MRO, pydoc), C/Python
- blender: 4 defects (node runtime, USD skel, shader, anim), C++

Mastodon CWE-1333 benchmark:
- test_mastodon_cwe1333.rb: validates (.+\.)? -> ([^@]+\.)? fix
  eliminates O(2^N) backtracking in email validator
2026-04-13 14:03:16 -04:00

123 lines
4.3 KiB
Ruby

# frozen_string_literal: true
#
# CWE-1333 ReDoS benchmark for Mastodon BlacklistedEmailValidator
#
# Tests both the VULNERABLE and PATCHED regex patterns against adversarial
# email inputs. The vulnerable pattern uses (.+\.)? which causes exponential
# backtracking. The patched pattern uses ([^@]+\.)? which eliminates ambiguity.
#
# Usage:
# ruby test_mastodon_cwe1333.rb
#
# Expected results:
# VULNERABLE pattern: timeout or >10s on adversarial input (skipped by default)
# PATCHED pattern: <1s on adversarial input of any length
require 'benchmark'
require 'timeout'
# Simulated admin-configured blocked domains (pipe-separated alternation)
DOMAINS = %w[example\\.com evil\\.org badactor\\.net spammer\\.io].join('|')
# VULNERABLE pattern: (.+\.)? allows .+ to match anything, creating exponential
# backtracking when no domain matches
VULNERABLE_BLACKLIST = Regexp.new("@(.+\\.)?(#{DOMAINS})", true)
VULNERABLE_WHITELIST = Regexp.new("@(.+\\.)?(#{DOMAINS})$", true)
# PATCHED pattern: ([^@]+\.)? restricts the character class, eliminating ambiguity
PATCHED_BLACKLIST = Regexp.new("@([^@]+\\.)?(#{DOMAINS})", true)
PATCHED_WHITELIST = Regexp.new("@([^@]+\\.)?(#{DOMAINS})$", true)
# Adversarial inputs: long strings after @ with no matching domain
# These force maximum backtracking in the vulnerable pattern
ADVERSARIAL_INPUTS = {
'short_30' => "user@#{'a' * 30}",
'medium_80' => "user@#{'a' * 80}",
'long_200' => "user@#{'a' * 200}",
'long_500' => "user@#{'a' * 500}",
}
# Legitimate inputs that should match
LEGITIMATE_INPUTS = {
'direct_match' => 'user@example.com',
'subdomain_match' => 'user@sub.example.com',
'deep_subdomain' => 'user@a.b.c.example.com',
}
# Non-matching but benign inputs
BENIGN_NOMATCH = {
'safe_nomatch' => 'user@gmail.com',
}
WALL_CLOCK_LIMIT = 1.0 # seconds: patched regex must finish under this
TIMEOUT_LIMIT = 5.0 # seconds: vulnerable regex gets this long before we kill it
puts "=" * 72
puts "CWE-1333 ReDoS Benchmark: Mastodon BlacklistedEmailValidator"
puts "=" * 72
# --- Test 1: Verify patched pattern still matches legitimate emails ---
puts "\n--- Correctness: patched pattern matches legitimate emails ---"
failures = []
LEGITIMATE_INPUTS.each do |label, email|
bl_match = email =~ PATCHED_BLACKLIST
wl_match = email =~ PATCHED_WHITELIST
status = (bl_match && wl_match) ? "PASS" : "FAIL"
failures << label unless bl_match && wl_match
puts " %-20s => blacklist:%s whitelist:%s [%s]" % [label, bl_match ? 'Y' : 'N', wl_match ? 'Y' : 'N', status]
end
BENIGN_NOMATCH.each do |label, email|
bl_match = email =~ PATCHED_BLACKLIST
wl_match = email =~ PATCHED_WHITELIST
status = (!bl_match && !wl_match) ? "PASS" : "FAIL"
failures << label if bl_match || wl_match
puts " %-20s => blacklist:%s whitelist:%s [%s]" % [label, bl_match ? 'Y' : 'N', wl_match ? 'Y' : 'N', status]
end
# --- Test 2: Patched pattern under adversarial input (must finish fast) ---
puts "\n--- Complexity gate: patched pattern, adversarial inputs ---"
ADVERSARIAL_INPUTS.each do |label, email|
elapsed = Benchmark.realtime do
email =~ PATCHED_BLACKLIST
email =~ PATCHED_WHITELIST
end
status = elapsed < WALL_CLOCK_LIMIT ? "PASS" : "FAIL"
failures << "patched_#{label}" unless elapsed < WALL_CLOCK_LIMIT
puts " %-20s => %.6fs [%s] (limit: %.1fs)" % [label, elapsed, status, WALL_CLOCK_LIMIT]
end
# --- Test 3: Vulnerable pattern under adversarial input (demonstrate the defect) ---
puts "\n--- Demonstration: vulnerable pattern, adversarial inputs ---"
puts " (each test limited to #{TIMEOUT_LIMIT}s timeout)"
ADVERSARIAL_INPUTS.each do |label, email|
begin
elapsed = nil
Timeout.timeout(TIMEOUT_LIMIT) do
elapsed = Benchmark.realtime do
email =~ VULNERABLE_BLACKLIST
end
end
if elapsed > WALL_CLOCK_LIMIT
puts " %-20s => %.6fs [SLOW as expected]" % [label, elapsed]
else
puts " %-20s => %.6fs [completed]" % [label, elapsed]
end
rescue Timeout::Error
puts " %-20s => TIMEOUT (>%.1fs) [VULNERABLE confirmed]" % [label, TIMEOUT_LIMIT]
end
end
# --- Summary ---
puts "\n" + "=" * 72
if failures.empty?
puts "RESULT: ALL TESTS PASSED"
puts " Patched regex handles adversarial input without backtracking."
exit 0
else
puts "RESULT: #{failures.size} FAILURE(S): #{failures.join(', ')}"
exit 1
end