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
123 lines
4.3 KiB
Ruby
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
|