Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master' into rules-list
Browse files Browse the repository at this point in the history
  • Loading branch information
RaneemAlRushud committed Nov 23, 2024
2 parents 98fe361 + d133a43 commit 6337130
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 107 deletions.
119 changes: 47 additions & 72 deletions lib/mini_defender/data/private_network_patterns.txt
Original file line number Diff line number Diff line change
@@ -1,89 +1,64 @@
# Basic localhost patterns
# RFC 2606 - Reserved Domain Names
# https://datatracker.ietf.org/doc/html/rfc2606
*.test
*.example
*.invalid
localhost
127.
::1
0.0.0.0
::
*.local
local.*
localhost.*
*.localhost
example.com
example.net
example.org

# IPv4 patterns
127.0.0.[0-9]+
# RFC 6761 - Special-Use Domain Names
# https://datatracker.ietf.org/doc/html/rfc6761
*.local
*.onion
*.home.arpa

# Private networks
# RFC 1918 - Private Address Space
# https://datatracker.ietf.org/doc/html/rfc1918
10.*
172.(1[6-9]|2[0-9]|3[0-1]).*
192.168.*
169.254.*

# Alternative representations
0177.0000.0000.0001
0x7f000001

# Dotless decimal
2130706433 # 127.0.0.1
3232235521 # 192.168.0.1
3232235777 # 192.168.1.1
# RFC 3330 - Special-Use IPv4 Addresses
# https://datatracker.ietf.org/doc/html/rfc3330
127.*
169.254.*
0.0.0.0

# IPv6 patterns
0:0:0:0:0:0:0:1
::0:1
[0:]+:[0:]*:[0:]*:[0:]*:[0:]*:[0:]*:[0:]*:1
# RFC 4291 - IPv6 Addressing Architecture
# https://datatracker.ietf.org/doc/html/rfc4291
::1
fe80:*
fc00:*
fd*
2001:db8:*
::ffff:127.*
::ffff:0:127.*
64:ff9b::127.*
::ffff:0:0:0
::
::*
::ffff:*

# RFC 4193 - Unique Local IPv6 Unicast Addresses
# https://datatracker.ietf.org/doc/html/rfc4193
fc00:*
fd00:*

# Domain variants
intranet
internal
# Common Internal Network Patterns (based on RFC 2606 and 6761)
*.intranet
*.internal
*.localhost
*.workgroup
*.corp
*.lan
*.private
*.home
site
*.dev
*.test
*.example
*.invalid
*.localdomain
*.domain.local

# mDNS and Bonjour
*.local.mesh
*.local.net
*.local.home
intranet.*
internal.*
corp.*
lan.*

# Additional internal patterns
*.virtual
*.vmware
*.virtualbox
*.docker
*.vagrant
*.vm
*.development
*.staging
*.testing
*.sandbox
# Development Environments (based on RFC 2606)
*.dev.test
*.staging.test
*.qa.test
dev.example.*
staging.example.*
qa.example.*

# Encoded variants
%6C%6F%63%61%6C%68%6F%73%74
%6c%6f%63%61%6c%68%6f%73%74
bG9jYWxob3N0
# RFC 6052 - IPv6 Translation
64:ff9b::*

# DNS rebinding protection
*.0sngrc.*
*.1u0n.*
*.10minutemail.*
*.burpcollaborator.*
*.interact.sh
# RFC 3986 - URI Encoded Forms
%6C%6F%63%61%6C%68%6F%73%74
3 changes: 2 additions & 1 deletion lib/mini_defender/rules/integer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ def passes?(attribute, value, validator)
end

# Remove leading zero so Integer will not treat it as octal
# Handle leading zeros while preserving both + and - signs
value = value
.to_s
.gsub(/^0+/, '')
.gsub(/^([+-])?0+(?=\d)/, '\1')

if @mode == 'relaxed'
value = normalize_digits(value)
Expand Down
127 changes: 94 additions & 33 deletions lib/mini_defender/rules/url.rb
Original file line number Diff line number Diff line change
@@ -1,79 +1,140 @@
# frozen_string_literal: true

require 'uri'
require 'resolv'
require_relative '../detectors/public_domain_detector'
require 'ipaddr'
require 'public_suffix'

class MiniDefender::Rules::Url < MiniDefender::Rule
ALLOWED_MODIFIERS = %w[https_only public no_ip]
ALLOWED_MODIFIERS = %w[https public not_ip not_private]

def initialize(modifiers = [])
@modifiers = Array(modifiers).map(&:to_s)
validate_modifiers! unless @modifiers.empty?

unless @modifiers.empty?
validate_modifiers!
end

@validation_error = nil
end

def self.signature
'url'
end

def self.make(args)
new(args)
def self.make(modifiers) # no need to raise an error when no modifier is entered; as 'url' rule checks URL structure on its own
new(modifiers)
end

def passes?(attribute, value, validator)
# TODO: warning: URI.regexp is obsolete; use URI::DEFAULT_PARSER.make_regexp instead
return false unless value.is_a?(String) && URI.regexp(%w[http https]).match?(value)
unless value.is_a?(String)
return false
end

begin
uri = URI.parse(value)

return true if @modifiers.empty?
if uri.host.blank? || uri.scheme.blank?
return false
end

unless %w[http https].include?(uri.scheme)
@validation_error = 'URL protocol must be HTTP or HTTPS.'
return false
end

if @modifiers.empty?
return true
end

if @modifiers.include?('https') && uri.scheme != 'https'
@validation_error = 'The URL must use HTTPS.'
return false
end

if @modifiers.include?('public') && (!PublicSuffix.valid?(uri.host) || private_network?(uri.host))
@validation_error = 'The URL must use a valid public domain.'
return false
end

if @modifiers.include?('not_ip') && ip_address?(uri.host)
@validation_error = 'IP addresses are not allowed in URLs.'
return false
end

return false if @modifiers.include?('https_only') && uri.scheme != 'https'
return false if @modifiers.include?('public') &&
!MiniDefender::Detectors::PublicDomainDetector.public_domain?(uri.host)
return false if @modifiers.include?('no_ip') && ip_address?(uri.host)
if @modifiers.include?('not_private') && private_network?(uri.host)
@validation_error = 'Private or reserved resources are not allowed.'
return false
end

true
rescue URI::InvalidURIError
@validation_error = 'The field must contain a valid URL.'
false
rescue PublicSuffix::Error
false
end
end

def message(_attribute, value, _validator)
return 'The field must contain a valid URL.' unless value.is_a?(String)

begin
uri = URI.parse(value)
return 'The field must contain a valid URL.' if @modifiers.empty?

return 'The URL must use HTTPS.' if @modifiers.include?('https_only') && uri.scheme != 'https'
def message(attribute, value, validator)
@validation_error || 'The field must contain a valid URL.'
end

if @modifiers.include?('public') &&
!MiniDefender::Detectors::PublicDomainDetector.public_domain?(uri.host)
return 'The URL must use a valid public domain.'
end
def private_network?(host)
unless host
return false
end

return 'IP addresses are not allowed in URLs.' if @modifiers.include?('no_ip') && ip_address?(uri.host)
host = host.downcase

'The field must contain a valid URL.'
rescue URI::InvalidURIError
'The field must contain a valid URL.'
end
private_patterns.any? { |pattern| pattern.match?(host) }
end

private

def validate_modifiers!
invalid_modifiers = @modifiers - ALLOWED_MODIFIERS
return if invalid_modifiers.empty?
if invalid_modifiers.empty?
return
end

raise ArgumentError, "Invalid URL modifiers: #{invalid_modifiers.join(', ')}"
end

def ip_address?(host)
return false unless host
unless host
return false
end

begin
IPAddr.new(host)
true
rescue IPAddr::InvalidAddressError
false
end
end

def private_patterns
# Cache the result in class variable to avoid loading again
# across multiple instances
@@private_patterns ||= begin
pattern_file = File.expand_path('../data/private_network_patterns.txt', __dir__)

!!(host =~ Resolv::IPv4::Regex || host =~ Resolv::IPv6::Regex)
File.readlines(pattern_file).filter_map do |line|
line = line.strip

if line.empty? || line.start_with?('#')
next
end

# Pattern => regex (once)
pattern = line
.gsub('.', '\.') # escape dots
.gsub('*', '.*') # wildcards => regex
.gsub('[0-9]+', '\d+') # convert number ranges
.gsub(/\[(.+?)\]/, '(\1)') # convert chars classes

Regexp.new("^#{pattern}$", Regexp::IGNORECASE)
end
end
end
end
2 changes: 1 addition & 1 deletion lib/mini_defender/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module MiniDefender
VERSION = "0.6.6"
VERSION = "0.6.8"
end
1 change: 1 addition & 0 deletions mini_defender.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'countries'
spec.add_runtime_dependency 'money'
spec.add_runtime_dependency 'marcel'
spec.add_runtime_dependency 'public_suffix'
end
46 changes: 46 additions & 0 deletions test/rules/integer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,50 @@ def test_relaxed_passes_with_mixed_digits
def test_passes_with_integer_with_leading_zero
assert @rule_relax.passes?('amount', '08', nil)
end

def test_passes_with_single_zero
assert @rule.passes?('amount', '0', nil)
assert_equal 0, @rule.coerce('0')
end

def test_passes_with_multiple_zeros
assert @rule.passes?('amount', '00', nil)
assert_equal 0, @rule.coerce('00')
end

# Test for leading zeros before other digits
def test_removes_leading_zeros_before_digits
assert @rule.passes?('amount', '01', nil)
assert_equal 1, @rule.coerce('01')

assert @rule.passes?('amount', '0123', nil)
assert_equal 123, @rule.coerce('0123')

assert @rule.passes?('amount', '00123', nil)
assert_equal 123, @rule.coerce('00123')
end

def test_handles_zeros_with_whitespace
assert @rule.passes?('amount', ' 0 ', nil)
assert_equal 0, @rule.coerce(' 0 ')

assert @rule.passes?('amount', ' 00 ', nil)
assert_equal 0, @rule.coerce(' 00 ')
end

def test_handles_negative_numbers_with_leading_zeros
assert @rule.passes?('amount', '-001', nil)
assert_equal(-1, @rule.coerce('-001'))

assert @rule.passes?('amount', '-00123', nil)
assert_equal(-123, @rule.coerce('-00123'))
end

def test_handles_positve_numbers_with_leading_zeros
assert @rule.passes?('amount', '+001', nil)
assert_equal(1, @rule.coerce('+001'))

assert @rule.passes?('amount', '+00123', nil)
assert_equal(123, @rule.coerce('+00123'))
end
end
Loading

0 comments on commit 6337130

Please sign in to comment.