Skip to content

Commit

Permalink
Change the custom URI parser to be a bit more conservative
Browse files Browse the repository at this point in the history
First try the default URI(), and if it fails relax the restrictions on
the path component as a fallback.
  • Loading branch information
knu committed Nov 1, 2023
1 parent 27cc46c commit 8930674
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 57 deletions.
8 changes: 4 additions & 4 deletions lib/http/cookie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def parse(set_cookie, origin, options = nil, &block)
logger = options[:logger]
created_at = options[:created_at]
end
origin = HTTP::Cookie::URIParser.instance.convert_to_uri(origin)
origin = HTTP::Cookie::URIParser.parse(origin)

[].tap { |cookies|
Scanner.new(set_cookie, logger).scan_set_cookie { |name, value, attrs|
Expand Down Expand Up @@ -456,7 +456,7 @@ def origin= origin
@origin.nil? or
raise ArgumentError, "origin cannot be changed once it is set"
# Delay setting @origin because #domain= or #path= may fail
origin = HTTP::Cookie::URIParser.instance.convert_to_uri(origin)
origin = HTTP::Cookie::URIParser.parse(origin)
if URI::HTTP === origin
self.domain ||= origin.host
self.path ||= (origin + './').path
Expand Down Expand Up @@ -549,7 +549,7 @@ def expire!
# Tests if it is OK to accept this cookie if it is sent from a given
# URI/URL, `uri`.
def acceptable_from_uri?(uri)
uri = HTTP::Cookie::URIParser.instance.convert_to_uri(uri)
uri = HTTP::Cookie::URIParser.parse(uri)
return false unless URI::HTTP === uri && uri.host
host = DomainName.new(uri.host)

Expand Down Expand Up @@ -586,7 +586,7 @@ def valid_for_uri?(uri)
if @domain.nil?
raise "cannot tell if this cookie is valid because the domain is unknown"
end
uri = HTTP::Cookie::URIParser.instance.convert_to_uri(uri)
uri = HTTP::Cookie::URIParser.parse(uri)
# RFC 6265 5.4
return false if secure? && !(URI::HTTPS === uri)
acceptable_from_uri?(uri) && HTTP::Cookie.path_match?(@path, uri.path)
Expand Down
69 changes: 28 additions & 41 deletions lib/http/cookie/uri_parser.rb
Original file line number Diff line number Diff line change
@@ -1,49 +1,36 @@
require 'singleton'
module HTTP::Cookie::URIParser
module_function

class HTTP::Cookie::URIParser
include Singleton
# Regular Expression taken from RFC 3986 Appendix B
URIREGEX = %r{
\A
(?: (?<scheme> [^:/?\#]+ ) : )?
(?: // (?<authority> [^/?\#]* ) )?
(?<path> [^?\#]* )
(?: \? (?<query> [^\#]* ) )?
(?: \# (?<fragment> .* ) )?
\z
}x

REGEXP = {
ABS_PATH: /\A[^?#]*\z/
}
# Escape RFC 3986 "reserved" characters minus valid characters for path
# More specifically, gen-delims minus "/" / "?" / "#"
def escape_path(path)
path.sub(/\A[^?#]+/) { |p| p.gsub(/[:\[\]@]+/) { |r| CGI.escape(r) } }
end

# Parse a URI string or object, relaxing the constraints on the path component
def parse(uri)
m = /
\A
(?<scheme>https?)
:\/\/
((?<userinfo>.*)@)?
(?<host>[^\/]+)
(:(?<port>\d+))?
(?<path>[^?#]*)
(\?(?<query>[^#]*))?
(\#(?<fragment>.*))?
/xi.match(uri.to_s)
URI(uri)
rescue URI::InvalidURIError
str = String.try_convert(uri) or
raise ArgumentError, "bad argument (expected URI object or URI string)"

# Not an absolute HTTP/HTTPS URI
return URI::DEFAULT_PARSER.parse(uri) unless m
m = URIREGEX.match(str) or raise

URI.scheme_list[m['scheme'].upcase].new(
m['scheme'],
m['userinfo'],
m['host'],
m['port'],
nil, # registry
m['path'],
nil, # opaque
m['query'],
m['fragment'],
self
)
end

def convert_to_uri(uri)
if uri.is_a?(URI::Generic)
uri
elsif uri = String.try_convert(uri)
parse(uri)
else
raise ArgumentError, "bad argument (expected URI object or URI string)"
end
path = m[:path]
str[m.begin(:path)...m.end(:path)] = escape_path(path)
uri = URI.parse(str)
uri.__send__(:set_path, path)
uri
end
end
2 changes: 1 addition & 1 deletion lib/http/cookie_jar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def each(uri = nil, &block) # :yield: cookie
block_given? or return enum_for(__method__, uri)

if uri
uri = HTTP::Cookie::URIParser.instance.convert_to_uri(uri)
uri = HTTP::Cookie::URIParser.parse(uri)
return self unless URI::HTTP === uri && uri.host
end

Expand Down
14 changes: 7 additions & 7 deletions test/test_http_cookie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ def test_origin=
}

cookie = HTTP::Cookie.new('a', 'b')
cookie.origin = HTTP::Cookie::URIParser.instance.parse('http://example.com/path[]/')
cookie.origin = HTTP::Cookie::URIParser.parse('http://example.com/path[]/')
assert_equal '/path[]/', cookie.path

cookie = HTTP::Cookie.new('a', 'b', :domain => '.example.com')
Expand Down Expand Up @@ -1033,14 +1033,14 @@ def test_valid_for_uri?
]
},
HTTP::Cookie.parse('a4=b; domain=example.com; path=/dir2[]/',
HTTP::Cookie::URIParser.instance.parse('http://example.com/dir[]/file.html')).first => {
HTTP::Cookie::URIParser.parse('http://example.com/dir[]/file.html')).first => {
true => [
HTTP::Cookie::URIParser.instance.parse('https://example.com/dir2[]/file.html'),
HTTP::Cookie::URIParser.instance.parse('http://example.com/dir2[]/file.html'),
HTTP::Cookie::URIParser.parse('https://example.com/dir2[]/file.html'),
HTTP::Cookie::URIParser.parse('http://example.com/dir2[]/file.html'),
],
false => [
HTTP::Cookie::URIParser.instance.parse('https://example.com/dir[]/file.html'),
HTTP::Cookie::URIParser.instance.parse('http://example.com/dir[]/file.html'),
HTTP::Cookie::URIParser.parse('https://example.com/dir[]/file.html'),
HTTP::Cookie::URIParser.parse('http://example.com/dir[]/file.html'),
'file:///dir2/test.html',
]
},
Expand Down Expand Up @@ -1091,7 +1091,7 @@ def test_valid_for_uri?
hash.each { |expected, urls|
urls.each { |url|
assert_equal expected, cookie.valid_for_uri?(url), '%s: %s' % [cookie.name, url]
assert_equal expected, cookie.valid_for_uri?(HTTP::Cookie::URIParser.instance.parse(url)), "%s: URI(%s)" % [cookie.name, url]
assert_equal expected, cookie.valid_for_uri?(HTTP::Cookie::URIParser.parse(url)), "%s: URI(%s)" % [cookie.name, url]
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions test/test_http_cookie_jar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ def test_save_session_cookies_yaml
end

def test_save_and_read_cookiestxt
url = HTTP::Cookie::URIParser.instance.convert_to_uri('https://rubyforge.org/foo[]/')
url = HTTP::Cookie::URIParser.parse('https://rubyforge.org/foo[]/')

# Add one cookie with an expiration date in the future
cookie = HTTP::Cookie.new(cookie_values)
Expand Down Expand Up @@ -657,7 +657,7 @@ def test_paths
end

def test_non_rfc3986_compliant_paths
url = HTTP::Cookie::URIParser.instance.convert_to_uri('http://RubyForge.org/login[]')
url = HTTP::Cookie::URIParser.parse('http://RubyForge.org/login[]')

values = cookie_values(:path => "/login[]", :expires => nil, :origin => url)

Expand All @@ -671,8 +671,8 @@ def test_non_rfc3986_compliant_paths
assert_equal(2, @jar.cookies(url).length)

# Make sure we don't get the cookie in a different path
assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.instance.convert_to_uri('http://RubyForge.org/hello[]')).length)
assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.instance.convert_to_uri('http://RubyForge.org/')).length)
assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.parse('http://RubyForge.org/hello[]')).length)
assert_equal(0, @jar.cookies(HTTP::Cookie::URIParser.parse('http://RubyForge.org/')).length)

# Expire the first cookie
@jar.add(HTTP::Cookie.new(values.merge( :expires => Time.now - (10 * 86400))))
Expand Down

0 comments on commit 8930674

Please sign in to comment.