From eaac9648a401f62fa96f7cda0587a084ee6ac80b Mon Sep 17 00:00:00 2001 From: Jack Lloyd Date: Thu, 29 Mar 2018 12:41:57 -0400 Subject: Fix bugs in wildcard matching We would incorrectly accept invalid matches for example b*.example.net could match foobar.example.net Introduced in 289cc25709b08 --- src/lib/utils/parsing.cpp | 184 +++++++++++++++++++++++++++------------------- 1 file changed, 107 insertions(+), 77 deletions(-) (limited to 'src/lib/utils/parsing.cpp') diff --git a/src/lib/utils/parsing.cpp b/src/lib/utils/parsing.cpp index 9517cc673..cfae0cb70 100644 --- a/src/lib/utils/parsing.cpp +++ b/src/lib/utils/parsing.cpp @@ -1,6 +1,6 @@ /* * Various string utils and parsing functions -* (C) 1999-2007,2013,2014,2015 Jack Lloyd +* (C) 1999-2007,2013,2014,2015,2018 Jack Lloyd * (C) 2015 Simon Warta (Kullo GmbH) * (C) 2017 René Korthaus, Rohde & Schwarz Cybersecurity * @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include @@ -338,110 +340,138 @@ std::string replace_char(const std::string& str, char from_char, char to_char) return out; } -bool host_wildcard_match(const std::string& issued, const std::string& host) +namespace { + +std::string tolower_string(const std::string& in) { - if(issued == host) + std::string s = in; + for(size_t i = 0; i != s.size(); ++i) { - return true; + if(std::isalpha(static_cast(s[i]))) + s[i] = std::tolower(static_cast(s[i])); } + return s; + } + +} + +bool host_wildcard_match(const std::string& issued_, const std::string& host_) + { + const std::string issued = tolower_string(issued_); + const std::string host = tolower_string(host_); + + if(host.empty() || issued.empty()) + return false; + + /* + If there are embedded nulls in your issued name + Well I feel bad for you son + */ + if(std::count(issued.begin(), issued.end(), char(0)) > 0) + return false; - size_t stars = 0; - for(char c : issued) + // If more than one wildcard, then issued name is invalid + const size_t stars = std::count(issued.begin(), issued.end(), '*'); + if(stars > 1) + return false; + + // '*' is not a valid character in DNS names so should not appear on the host side + if(std::count(host.begin(), host.end(), '*') != 0) + return false; + + // Similarly a DNS name can't end in . + if(host[host.size() - 1] == '.') + return false; + + // And a host can't have an empty name component, so reject that + if(host.find("..") != std::string::npos) + return false; + + // Exact match: accept + if(issued == host) { - if(c == '*') - stars += 1; + return true; } - if(stars > 1) + /* + Otherwise it might be a wildcard + + If the issued size is strictly longer than the hostname size it + couldn't possibly be a match, even if the issued value is a + wildcard. The only exception is when the wildcard ends up empty + (eg www.example.com matches www*.example.com) + */ + if(issued.size() > host.size() + 1) { return false; } - // first try to match the base, then the left-most label - // which can contain exactly one wildcard at any position - if(issued.size() > 2) + // If no * at all then not a wildcard, and so not a match + if(stars != 1) { - size_t host_i = host.find('.'); - if(host_i == std::string::npos || host_i == host.size() - 1) - { - return false; - } - - size_t issued_i = issued.find('.'); - if(issued_i == std::string::npos || issued_i == issued.size() - 1) - { - return false; - } + return false; + } - const std::string host_base = host.substr(host_i + 1); - const std::string issued_base = issued.substr(issued_i + 1); + /* + Now walk through the issued string, making sure every character + matches. When we come to the (singular) '*', jump forward in the + hostname by the cooresponding amount. We know exactly how much + space the wildcard takes because it must be exactly `len(host) - + len(issued) + 1 chars`. - // if anything but the left-most label doesn't equal, - // we are already out here - if(host_base != issued_base) - { - return false; - } + We also verify that the '*' comes in the leftmost component, and + doesn't skip over any '.' in the hostname. + */ + size_t dots_seen = 0; + size_t host_idx = 0; - // compare the left-most labels - std::string host_prefix = host.substr(0, host_i); + for(size_t i = 0; i != issued.size(); ++i) + { + dots_seen += (issued[i] == '.'); - if(host_prefix.empty()) + if(issued[i] == '*') { - return false; - } + // Fail: wildcard can only come in leftmost component + if(dots_seen > 0) + { + return false; + } - const std::string issued_prefix = issued.substr(0, issued_i); + /* + Since there is only one * we know the tail of the issued and + hostname must be an exact match. In this case advance host_idx + to match. + */ + const size_t advance = (host.size() - issued.size() + 1); - // if split_on would work on strings with less than 2 items, - // the if/else block would not be necessary - if(issued_prefix == "*") - { - return true; - } + if(host_idx + advance > host.size()) // shouldn't happen + return false; - std::vector p; + // Can't be any intervening .s that we would have skipped + if(std::count(host.begin() + host_idx, + host.begin() + host_idx + advance, '.') != 0) + return false; - if(issued_prefix[0] == '*') - { - p = std::vector{"", issued_prefix.substr(1, issued_prefix.size())}; - } - else if(issued_prefix[issued_prefix.size()-1] == '*') - { - p = std::vector{issued_prefix.substr(0, issued_prefix.size() - 1), ""}; + host_idx += advance; } else { - p = split_on(issued_prefix, '*'); - } - - if(p.size() != 2) - { - return false; - } - - // match anything before and after the wildcard character - const std::string first = p[0]; - const std::string last = p[1]; + if(issued[i] != host[host_idx]) + { + return false; + } - if(host_prefix.substr(0, first.size()) == first) - { - host_prefix.erase(0, first.size()); - } - - // nothing to match anymore - if(last.empty()) - { - return true; + host_idx += 1; } + } - if(host_prefix.size() >= last.size() && - host_prefix.substr(host_prefix.size() - last.size(), last.size()) == last) - { - return true; - } + // Wildcard issued name must have at least 3 components + if(dots_seen < 2) + { + return false; } - return false; + return true; } + } -- cgit v1.2.3