From 7a7f37f146aa977350cf914eba20a95ce371485f Mon Sep 17 00:00:00 2001 From: sabulikia Date: Thu, 7 Jul 2022 16:10:20 -0400 Subject: [PATCH] Use string#split instead of regex for domain parts [CVE-2023-22792] --- .../lib/action_dispatch/middleware/cookies.rb | 48 +++++++++++-------- actionpack/test/dispatch/cookies_test.rb | 26 ++++++++++ 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index ac5844723303a..335122adb5c73 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -283,20 +283,6 @@ def signed_cookie_digest class CookieJar #:nodoc: include Enumerable, ChainedCookieJars - # This regular expression is used to split the levels of a domain. - # The top level domain can be any string without a period or - # **.**, ***.** style TLDs like co.uk or com.au - # - # www.example.co.uk gives: - # $& => example.co.uk - # - # example.com gives: - # $& => example.com - # - # lots.of.subdomains.example.local gives: - # $& => example.local - DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.build(req, cookies) jar = new(req) jar.update(cookies) @@ -449,13 +435,35 @@ def handle_options(options) options[:same_site] ||= cookies_same_site_protection.call(request) if options[:domain] == :all || options[:domain] == "all" - # If there is a provided tld length then we use it otherwise default domain regexp. - domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP + cookie_domain = "" + dot_splitted_host = request.host.split('.', -1) + + # Case where request.host is not an IP address or it's an invalid domain + # (ip confirms to the domain structure we expect so we explicitly check for ip) + if request.host.match?(/^[\d.]+$/) || dot_splitted_host.include?("") || dot_splitted_host.length == 1 + options[:domain] = nil + return + end + + # If there is a provided tld length then we use it otherwise default domain. + if options[:tld_length].present? + # Case where the tld_length provided is valid + if dot_splitted_host.length >= options[:tld_length] + cookie_domain = dot_splitted_host.last(options[:tld_length]).join('.') + end + # Case where tld_length is not provided + else + # Regular TLDs + if !(/([^.]{2,3}\.[^.]{2})$/.match?(request.host)) + cookie_domain = dot_splitted_host.last(2).join('.') + # **.**, ***.** style TLDs like co.uk and com.au + else + cookie_domain = dot_splitted_host.last(3).join('.') + end + end - # If host is not ip and matches domain regexp. - # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp) - ".#{$&}" + options[:domain] = if cookie_domain.present? + ".#{cookie_domain}" end elsif options[:domain].is_a? Array # If host matches one of the supplied domains.