Class: RailsAi::Security::InputValidator
- Inherits:
-
Object
- Object
- RailsAi::Security::InputValidator
- Defined in:
- lib/rails_ai/security/input_validator.rb
Constant Summary collapse
- MAX_FILE_SIZE =
50MB in bytes
50 * 1024 * 1024
- ALLOWED_IMAGE_TYPES =
%w[image/jpeg image/png image/gif image/webp].freeze
- ALLOWED_VIDEO_TYPES =
%w[video/mp4 video/webm video/quicktime].freeze
- ALLOWED_AUDIO_TYPES =
%w[audio/mpeg audio/wav audio/ogg audio/mp4].freeze
- ALLOWED_SCHEMES =
%w[http https].freeze
- BLOCKED_HOSTS =
%w[ localhost 127.0.0.1 0.0.0.0 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 metadata.google.internal instance-data.ec2.internal ].freeze
Class Method Summary collapse
- .blocked_host?(host) ⇒ Boolean
- .private_ip?(host) ⇒ Boolean
- .valid_mime_type?(mime_type, expected_type) ⇒ Boolean
- .validate_base64_data(data, expected_type: nil) ⇒ Object
- .validate_file_path(path) ⇒ Object
- .validate_message(message, index) ⇒ Object
- .validate_messages(messages) ⇒ Object
- .validate_text_input(text, max_length: 10000) ⇒ Object
- .validate_url(url) ⇒ Object
Class Method Details
.blocked_host?(host) ⇒ Boolean
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/rails_ai/security/input_validator.rb', line 147 def self.blocked_host?(host) return true if host.nil? BLOCKED_HOSTS.any? do |blocked| if blocked.include?('/') begin blocked_network = IPAddr.new(blocked) blocked_network.include?(host) rescue IPAddr::InvalidAddressError false end else host == blocked || host.end_with?(".#{blocked}") end end end |
.private_ip?(host) ⇒ Boolean
164 165 166 167 168 169 170 171 172 173 |
# File 'lib/rails_ai/security/input_validator.rb', line 164 def self.private_ip?(host) return false if host.nil? begin ip = IPAddr.new(host) ip.private? || ip.loopback? rescue IPAddr::InvalidAddressError false end end |
.valid_mime_type?(mime_type, expected_type) ⇒ Boolean
134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/rails_ai/security/input_validator.rb', line 134 def self.valid_mime_type?(mime_type, expected_type) case expected_type when :image ALLOWED_IMAGE_TYPES.include?(mime_type) when :video ALLOWED_VIDEO_TYPES.include?(mime_type) when :audio ALLOWED_AUDIO_TYPES.include?(mime_type) else true end end |
.validate_base64_data(data, expected_type: nil) ⇒ Object
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/rails_ai/security/input_validator.rb', line 56 def self.validate_base64_data(data, expected_type: nil) raise ValidationError, "Base64 data cannot be nil" if data.nil? unless data.match?(/\A[A-Za-z0-9+\/]*={0,2}\z/) raise ValidationError, "Invalid base64 format" end begin decoded = Base64.strict_decode64(data) if decoded.size > MAX_FILE_SIZE raise ValidationError, "Base64 data too large (max: #{MAX_FILE_SIZE} bytes)" end rescue ArgumentError raise ValidationError, "Invalid base64 data" end if expected_type && data.start_with?("data:") mime_type = data.split(';')[0].split(':')[1] unless valid_mime_type?(mime_type, expected_type) raise ValidationError, "Invalid MIME type: #{mime_type}" end end data end |
.validate_file_path(path) ⇒ Object
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/rails_ai/security/input_validator.rb', line 36 def self.validate_file_path(path) raise ValidationError, "File path cannot be nil" if path.nil? path = File.(path) if path.include?('..') || path.include?('~') raise ValidationError, "Invalid file path: directory traversal detected" end unless File.exist?(path) && File.readable?(path) raise ValidationError, "File not found or not readable: #{path}" end if File.size(path) > MAX_FILE_SIZE raise ValidationError, "File too large (max: #{MAX_FILE_SIZE} bytes)" end path end |
.validate_message(message, index) ⇒ Object
121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rails_ai/security/input_validator.rb', line 121 def self.(, index) raise ValidationError, "Message #{index} must be a hash" unless .is_a?(Hash) raise ValidationError, "Message #{index} missing role" unless [:role] raise ValidationError, "Message #{index} missing content" unless [:content] valid_roles = %w[system user assistant] unless valid_roles.include?([:role]) raise ValidationError, "Message #{index} has invalid role: #{[:role]}" end validate_text_input([:content], max_length: 50000) end |
.validate_messages(messages) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/rails_ai/security/input_validator.rb', line 106 def self.() raise ValidationError, "Messages cannot be nil" if .nil? raise ValidationError, "Messages must be an array" unless .is_a?(Array) raise ValidationError, "Messages cannot be empty" if .empty? raise ValidationError, "Too many messages (max: 100)" if .length > 100 .each_with_index do |, index| (, index) end end |
.validate_text_input(text, max_length: 10000) ⇒ Object
22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/rails_ai/security/input_validator.rb', line 22 def self.validate_text_input(text, max_length: 10000) raise ValidationError, "Text cannot be nil" if text.nil? text = text.to_s.strip raise ValidationError, "Text cannot be empty" if text.empty? raise ValidationError, "Text too long (max: #{max_length})" if text.length > max_length if text.match?(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/) raise ValidationError, "Text contains invalid characters" end text end |
.validate_url(url) ⇒ Object
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rails_ai/security/input_validator.rb', line 82 def self.validate_url(url) raise ValidationError, "URL cannot be nil" if url.nil? begin uri = URI.parse(url) rescue URI::InvalidURIError raise ValidationError, "Invalid URL format" end unless ALLOWED_SCHEMES.include?(uri.scheme) raise ValidationError, "Invalid URL scheme: #{uri.scheme}" end if blocked_host?(uri.host) raise ValidationError, "URL blocked for security reasons" end if private_ip?(uri.host) raise ValidationError, "Private IP addresses not allowed" end url end |