Skip to content

Commit c031b9c

Browse files
committed
Disallow invalid or private URIs
1 parent a3f5340 commit c031b9c

File tree

3 files changed

+108
-2
lines changed

3 files changed

+108
-2
lines changed

fasp_base/app/models/fasp_base/registration.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ class Registration
1515
validates :email,
1616
presence: true,
1717
format: { with: /@/ }
18-
# TODO: Proper URL validation
1918
validates :base_url,
20-
presence: true
19+
presence: true,
20+
"fasp_base/uri": { if: -> { Rails.env.production? } }
2121
validates :password,
2222
presence: true,
2323
confirmation: true,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module FaspBase
2+
class UriValidator < ActiveModel::EachValidator
3+
def validate_each(record, attribute, value)
4+
record.errors.add(attribute, :invalid) unless uri_valid?(value)
5+
end
6+
7+
private
8+
9+
def uri_valid?(string)
10+
uri = URI.parse(string)
11+
12+
return false unless scheme_valid?(uri)
13+
return false unless host_valid?(uri.host)
14+
15+
true
16+
rescue URI::Error
17+
false
18+
end
19+
20+
def scheme_valid?(uri)
21+
return false unless uri.is_a?(URI::HTTP)
22+
return false unless options[:allow_http] || uri.is_a?(URI::HTTPS)
23+
24+
true
25+
end
26+
27+
def host_valid?(host)
28+
return false unless host.present?
29+
30+
addresses = resolve_addresses(host)
31+
return false if addresses.empty?
32+
33+
non_private?(addresses)
34+
end
35+
36+
def resolve_addresses(host)
37+
Socket.getaddrinfo(host, nil)
38+
.map { |info| info[3] }
39+
.uniq
40+
rescue Socket::ResolutionError
41+
[]
42+
end
43+
44+
def non_private?(addresses)
45+
addresses.none? do |address|
46+
ipaddr = IPAddr.new(address)
47+
48+
ipaddr.loopback? || ipaddr.link_local? || ipaddr.private?
49+
end
50+
end
51+
end
52+
end
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
require "test_helper"
2+
3+
module FaspBase
4+
class UriValidatorTest < ::ActiveSupport::TestCase
5+
class Website
6+
include ActiveModel::Model
7+
include ActiveModel::Attributes
8+
9+
attribute :uri, :string
10+
11+
validates :uri, 'fasp_base/uri': true
12+
end
13+
14+
setup do
15+
@website = Website.new
16+
end
17+
18+
test "non-https URIs are invalid" do
19+
invalid_uris = %w[ ftp://example.com file:///root/ ]
20+
21+
invalid_uris.each do |uri|
22+
@website.uri = uri
23+
24+
assert @website.invalid?
25+
end
26+
end
27+
28+
test "missing, incomplete or syntactically wrong URIs are invalid" do
29+
invalid_uris = [ nil, "", "abc", "www.test.com", "https///test" ]
30+
31+
invalid_uris.each do |uri|
32+
@website.uri = uri
33+
34+
assert @website.invalid?
35+
end
36+
end
37+
38+
test "URI with hostnames resolving to private IPs are invalid" do
39+
local_uris = %w[ http://127.0.0.1/test https://127.0.0.123/other http://localhost:3000/base ]
40+
41+
local_uris.each do |uri|
42+
@website.uri = uri
43+
44+
assert @website.invalid?
45+
end
46+
end
47+
48+
test "external, existing URI is valid" do
49+
@website.uri = "https://github.com/mastodon"
50+
51+
assert @website.valid?
52+
end
53+
end
54+
end

0 commit comments

Comments
 (0)