From f079ce18fe023ae0d2c3734429da4677c2cdd4df Mon Sep 17 00:00:00 2001 From: Zoltan Dezso Date: Thu, 17 Jan 2013 13:50:57 +0900 Subject: [PATCH] [FIX] Make route parsing regex more robust - fixes sinatra/sinatra/#611 - adds support for case-insensitive URL encoding --- lib/sinatra/base.rb | 32 ++++++++++++++++++++++++++++++-- test/compile_test.rb | 28 +++++++++++++++++++++------- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/lib/sinatra/base.rb b/lib/sinatra/base.rb index 1d46a8d5c5..b920937e2a 100644 --- a/lib/sinatra/base.rb +++ b/lib/sinatra/base.rb @@ -1325,7 +1325,10 @@ def compile(path) ignore = "" pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) do |c| ignore << escaped(c).join if c.match(/[\.@]/) - encoded(c) + patt = encoded(c) + patt.gsub(/%\h\h/) do |match| + match.split(//).map {|char| char =~ /[A-Z]/ ? "[#{char}#{char.tr('A-Z', 'a-z')}]" : char}.join + end end pattern.gsub!(/((:\w+)|\*)/) do |match| if match == "*" @@ -1333,7 +1336,9 @@ def compile(path) "(.*?)" else keys << $2[1..-1] - "([^#{ignore}/?#]+)" + ignore_pattern = safe_ignore(ignore) + + ignore_pattern end end [/\A#{pattern}\z/, keys] @@ -1361,6 +1366,29 @@ def escaped(char, enc = URI.escape(char)) [Regexp.escape(enc), URI.escape(char, /./)] end + def safe_ignore(ignore) + unsafe_ignore = [] + ignore = ignore.gsub(/%\h\h/) do |hex| + unsafe_ignore << hex[1..2] + '' + end + unsafe_patterns = unsafe_ignore.map do |unsafe| + chars = unsafe.split(//).map do |char| + if char =~ /[A-Z]/ + char <<= char.tr('A-Z', 'a-z') + end + char + end + + "|(?:%[^#{chars[0]}].|%[#{chars[0]}][^#{chars[1]}])" + end + if unsafe_patterns.length > 0 + "((?:[^#{ignore}/?#%]#{unsafe_patterns.join()})+)" + else + "([^#{ignore}/?#]+)" + end + end + public # Makes the methods defined in the block and in the Modules given # in `extensions` available to the handlers and templates diff --git a/test/compile_test.rb b/test/compile_test.rb index 2344d3aaae..35d3437853 100644 --- a/test/compile_test.rb +++ b/test/compile_test.rb @@ -58,7 +58,7 @@ def compiled pattern fails "/:foo", "/" fails "/:foo", "/foo/" - converts "/föö", %r{\A/f%C3%B6%C3%B6\z} + converts "/föö", %r{\A/f%[Cc]3%[Bb]6%[Cc]3%[Bb]6\z} parses "/föö", "/f%C3%B6%C3%B6", {} converts "/:foo/:bar", %r{\A/([^/?#]+)/([^/?#]+)\z} @@ -87,7 +87,7 @@ def compiled pattern converts "/test$/", %r{\A/test(?:\$|%24)/\z} parses "/test$/", "/test$/", {} - converts "/te+st/", %r{\A/te(?:\+|%2B)st/\z} + converts "/te+st/", %r{\A/te(?:\+|%2[Bb])st/\z} parses "/te+st/", "/te+st/", {} fails "/te+st/", "/test/" fails "/te+st/", "/teeest/" @@ -95,7 +95,7 @@ def compiled pattern converts "/test(bar)/", %r{\A/test(?:\(|%28)bar(?:\)|%29)/\z} parses "/test(bar)/", "/test(bar)/", {} - converts "/path with spaces", %r{\A/path(?:%20|(?:\+|%2B))with(?:%20|(?:\+|%2B))spaces\z} + converts "/path with spaces", %r{\A/path(?:%20|(?:\+|%2[Bb]))with(?:%20|(?:\+|%2[Bb]))spaces\z} parses "/path with spaces", "/path%20with%20spaces", {} parses "/path with spaces", "/path%2Bwith%2Bspaces", {} parses "/path with spaces", "/path+with+spaces", {} @@ -110,22 +110,22 @@ def compiled pattern parses "/*/foo/*/*", "/bar/foo/bling/baz/boom", "splat" => ["bar", "bling", "baz/boom"] fails "/*/foo/*/*", "/bar/foo/baz" - converts "/test.bar", %r{\A/test(?:\.|%2E)bar\z} + converts "/test.bar", %r{\A/test(?:\.|%2[Ee])bar\z} parses "/test.bar", "/test.bar", {} fails "/test.bar", "/test0bar" - converts "/:file.:ext", %r{\A/([^\.%2E/?#]+)(?:\.|%2E)([^\.%2E/?#]+)\z} + converts "/:file.:ext", %r{\A/((?:[^\./?#%]|(?:%[^2].|%[2][^Ee]))+)(?:\.|%2[Ee])((?:[^\./?#%]|(?:%[^2].|%[2][^Ee]))+)\z} parses "/:file.:ext", "/pony.jpg", "file" => "pony", "ext" => "jpg" parses "/:file.:ext", "/pony%2Ejpg", "file" => "pony", "ext" => "jpg" fails "/:file.:ext", "/.jpg" - converts "/:name.?:format?", %r{\A/([^\.%2E/?#]+)(?:\.|%2E)?([^\.%2E/?#]+)?\z} + converts "/:name.?:format?", %r{\A/((?:[^\./?#%]|(?:%[^2].|%[2][^Ee]))+)(?:\.|%2[Ee])?((?:[^\./?#%]|(?:%[^2].|%[2][^Ee]))+)?\z} parses "/:name.?:format?", "/foo", "name" => "foo", "format" => nil parses "/:name.?:format?", "/foo.bar", "name" => "foo", "format" => "bar" parses "/:name.?:format?", "/foo%2Ebar", "name" => "foo", "format" => "bar" fails "/:name.?:format?", "/.bar" - converts "/:user@?:host?", %r{\A/([^@%40/?#]+)(?:@|%40)?([^@%40/?#]+)?\z} + converts "/:user@?:host?", %r{\A/((?:[^@/?#%]|(?:%[^4].|%[4][^0]))+)(?:@|%40)?((?:[^@/?#%]|(?:%[^4].|%[4][^0]))+)?\z} parses "/:user@?:host?", "/foo@bar", "user" => "foo", "host" => "bar" parses "/:user@?:host?", "/foo.foo@bar", "user" => "foo.foo", "host" => "bar" parses "/:user@?:host?", "/foo@bar.bar", "user" => "foo", "host" => "bar.bar" @@ -136,4 +136,18 @@ def compiled pattern # parses "/:name(.:format)?", "/foo", "name" => "foo", "format" => nil # parses "/:name(.:format)?", "/foo.bar", "name" => "foo", "format" => "bar" fails "/:name(.:format)?", "/foo." + + parses "/:id/test.bar", "/3/test.bar", {"id" => "3"} + parses "/:id/test.bar", "/2/test.bar", {"id" => "2"} + parses "/:id/test.bar", "/2E/test.bar", {"id" => "2E"} + parses "/:id/test.bar", "/2e/test.bar", {"id" => "2e"} + fails "/:id/test.bar", "/%2E/test.bar" + + parses "/:file.:ext", "/pony%2ejpg", "file" => "pony", "ext" => "jpg" + parses "/:file.:ext", "/pony%E6%AD%A3%2Ejpg", "file" => "pony%E6%AD%A3", "ext" => "jpg" + parses "/:file.:ext", "/pony%e6%ad%a3%2ejpg", "file" => "pony%e6%ad%a3", "ext" => "jpg" + parses "/:file.:ext", "/pony正%2Ejpg", "file" => "pony正", "ext" => "jpg" + parses "/:file.:ext", "/pony正%2ejpg", "file" => "pony正", "ext" => "jpg" + fails "/:file.:ext", "/pony正..jpg" + fails "/:file.:ext", "/pony正.%2ejpg" end