From f2d15935d2cb4caef05640807ec2018552374655 Mon Sep 17 00:00:00 2001 From: edgul Date: Thu, 5 Sep 2024 21:07:34 +0000 Subject: [PATCH] Bug 1914141 - Added an ending-in-number check for non-ipv4 domains for Services.io.isValidHostname. r=necko-reviewers,valentin Differential Revision: https://phabricator.services.mozilla.com/D220488 --- dom/html/HTMLDNSPrefetch.cpp | 8 +- dom/privateattribution/PrivateAttribution.cpp | 2 +- netwerk/base/Dashboard.cpp | 2 +- netwerk/base/IPv4Parser.cpp | 298 +++++++++++++++++ netwerk/base/IPv4Parser.h | 28 ++ netwerk/base/moz.build | 2 + netwerk/base/nsIIOService.idl | 5 +- netwerk/base/nsIOService.cpp | 24 +- netwerk/base/nsSocketTransport2.cpp | 2 +- netwerk/base/nsStandardURL.cpp | 315 +----------------- netwerk/base/nsStandardURL.h | 2 - netwerk/base/nsURLHelper.cpp | 2 +- netwerk/base/nsURLHelper.h | 2 +- netwerk/dns/nsHostResolver.cpp | 2 +- netwerk/protocol/http/TRRServiceChannel.cpp | 2 +- netwerk/protocol/http/nsHttpChannel.cpp | 2 +- netwerk/test/gtest/TestStandardURL.cpp | 18 +- netwerk/test/unit/test_standardurl.js | 16 + 18 files changed, 396 insertions(+), 336 deletions(-) create mode 100644 netwerk/base/IPv4Parser.cpp create mode 100644 netwerk/base/IPv4Parser.h diff --git a/dom/html/HTMLDNSPrefetch.cpp b/dom/html/HTMLDNSPrefetch.cpp index b9c97b2d2f34..39984d3534fe 100644 --- a/dom/html/HTMLDNSPrefetch.cpp +++ b/dom/html/HTMLDNSPrefetch.cpp @@ -229,10 +229,10 @@ nsresult HTMLDNSPrefetch::Prefetch( const OriginAttributes& aPartitionedPrincipalOriginAttributes, nsIDNSService::DNSFlags flags) { if (IsNeckoChild()) { - // We need to check IsEmpty() because net_IsValidHostName() + // We need to check IsEmpty() because net_IsValidDNSHost() // considers empty strings to be valid hostnames if (!hostname.IsEmpty() && - net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) { + net_IsValidDNSHost(NS_ConvertUTF16toUTF8(hostname))) { // during shutdown gNeckoChild might be null if (gNeckoChild) { gNeckoChild->SendHTMLDNSPrefetch( @@ -312,10 +312,10 @@ nsresult HTMLDNSPrefetch::CancelPrefetch( nsIDNSService::DNSFlags flags, nsresult aReason) { // Forward this request to Necko Parent if we're a child process if (IsNeckoChild()) { - // We need to check IsEmpty() because net_IsValidHostName() + // We need to check IsEmpty() because net_IsValidDNSHost() // considers empty strings to be valid hostnames if (!hostname.IsEmpty() && - net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) { + net_IsValidDNSHost(NS_ConvertUTF16toUTF8(hostname))) { // during shutdown gNeckoChild might be null if (gNeckoChild && gNeckoChild->CanSend()) { gNeckoChild->SendCancelHTMLDNSPrefetch( diff --git a/dom/privateattribution/PrivateAttribution.cpp b/dom/privateattribution/PrivateAttribution.cpp index cd7244d42bf4..f6872008e653 100644 --- a/dom/privateattribution/PrivateAttribution.cpp +++ b/dom/privateattribution/PrivateAttribution.cpp @@ -53,7 +53,7 @@ bool PrivateAttribution::GetSourceHostIfNonPrivate(nsACString& aSourceHost, [[nodiscard]] static bool ValidateHost(const nsACString& aHost, ErrorResult& aRv) { - if (!net_IsValidHostName(aHost)) { + if (!net_IsValidDNSHost(aHost)) { aRv.ThrowSyntaxError(aHost + " is not a valid host name"_ns); return false; } diff --git a/netwerk/base/Dashboard.cpp b/netwerk/base/Dashboard.cpp index 75a45a476068..5ed3abb1cf27 100644 --- a/netwerk/base/Dashboard.cpp +++ b/netwerk/base/Dashboard.cpp @@ -1115,7 +1115,7 @@ nsresult Dashboard::TestNewConnection(ConnectionData* aConnectionData) { nsresult rv; if (!connectionData->mHost.Length() || - !net_IsValidHostName(connectionData->mHost)) { + !net_IsValidDNSHost(connectionData->mHost)) { return NS_ERROR_UNKNOWN_HOST; } diff --git a/netwerk/base/IPv4Parser.cpp b/netwerk/base/IPv4Parser.cpp new file mode 100644 index 000000000000..3a0f472cb68e --- /dev/null +++ b/netwerk/base/IPv4Parser.cpp @@ -0,0 +1,298 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "IPv4Parser.h" +#include "mozilla/EndianUtils.h" +#include "nsPrintfCString.h" +#include "nsTArray.h" + +namespace mozilla::net::IPv4Parser { + +// https://url.spec.whatwg.org/#ends-in-a-number-checker +bool EndsInANumber(const nsCString& input) { + // 1. Let parts be the result of strictly splitting input on U+002E (.). + nsTArray parts; + for (const nsDependentCSubstring& part : input.Split('.')) { + parts.AppendElement(part); + } + + if (parts.Length() == 0) { + return false; + } + + // 2.If the last item in parts is the empty string, then: + // 1. If parts’s size is 1, then return false. + // 2. Remove the last item from parts. + if (parts.LastElement().IsEmpty()) { + if (parts.Length() == 1) { + return false; + } + Unused << parts.PopLastElement(); + } + + // 3. Let last be the last item in parts. + const nsDependentCSubstring& last = parts.LastElement(); + + // 4. If last is non-empty and contains only ASCII digits, then return true. + // The erroneous input "09" will be caught by the IPv4 parser at a later + // stage. + if (!last.IsEmpty()) { + if (ContainsOnlyAsciiDigits(last)) { + return true; + } + } + + // 5. If parsing last as an IPv4 number does not return failure, then return + // true. This is equivalent to checking that last is "0X" or "0x", followed by + // zero or more ASCII hex digits. + if (StringBeginsWith(last, "0x"_ns) || StringBeginsWith(last, "0X"_ns)) { + if (ContainsOnlyAsciiHexDigits(Substring(last, 2))) { + return true; + } + } + + return false; +} + +nsresult ParseIPv4Number10(const nsACString& input, uint32_t& number, + uint32_t maxNumber) { + uint64_t value = 0; + const char* current = input.BeginReading(); + const char* end = input.EndReading(); + for (; current < end; ++current) { + char c = *current; + MOZ_ASSERT(c >= '0' && c <= '9'); + value *= 10; + value += c - '0'; + } + if (value <= maxNumber) { + number = value; + return NS_OK; + } + + // The error case + number = 0; + return NS_ERROR_FAILURE; +} + +nsresult ParseIPv4Number(const nsACString& input, int32_t base, + uint32_t& number, uint32_t maxNumber) { + // Accumulate in the 64-bit value + uint64_t value = 0; + const char* current = input.BeginReading(); + const char* end = input.EndReading(); + switch (base) { + case 16: + ++current; + [[fallthrough]]; + case 8: + ++current; + break; + case 10: + default: + break; + } + for (; current < end; ++current) { + value *= base; + char c = *current; + MOZ_ASSERT((base == 10 && IsAsciiDigit(c)) || + (base == 8 && c >= '0' && c <= '7') || + (base == 16 && IsAsciiHexDigit(c))); + if (IsAsciiDigit(c)) { + value += c - '0'; + } else if (c >= 'a' && c <= 'f') { + value += c - 'a' + 10; + } else if (c >= 'A' && c <= 'F') { + value += c - 'A' + 10; + } + } + + if (value <= maxNumber) { + number = value; + return NS_OK; + } + + // The error case + number = 0; + return NS_ERROR_FAILURE; +} + +// IPv4 parser spec: https://url.spec.whatwg.org/#concept-ipv4-parser +nsresult NormalizeIPv4(const nsACString& host, nsCString& result) { + int32_t bases[4] = {10, 10, 10, 10}; + bool onlyBase10 = true; // Track this as a special case + int32_t dotIndex[3]; // The positions of the dots in the string + + // Use "length" rather than host.Length() after call to + // ValidateIPv4Number because of potential trailing period. + nsDependentCSubstring filteredHost; + bool trailingDot = false; + if (host.Length() > 0 && host.Last() == '.') { + trailingDot = true; + filteredHost.Rebind(host.BeginReading(), host.Length() - 1); + } else { + filteredHost.Rebind(host.BeginReading(), host.Length()); + } + + int32_t length = static_cast(filteredHost.Length()); + int32_t dotCount = ValidateIPv4Number(filteredHost, bases, dotIndex, + onlyBase10, length, trailingDot); + if (dotCount < 0 || length <= 0) { + return NS_ERROR_FAILURE; + } + + // Max values specified by the spec + static const uint32_t upperBounds[] = {0xffffffffu, 0xffffffu, 0xffffu, + 0xffu}; + uint32_t ipv4; + int32_t start = (dotCount > 0 ? dotIndex[dotCount - 1] + 1 : 0); + + // parse the last part first + nsresult res; + // Doing a special case for all items being base 10 gives ~35% speedup + res = (onlyBase10 + ? ParseIPv4Number10(Substring(host, start, length - start), ipv4, + upperBounds[dotCount]) + : ParseIPv4Number(Substring(host, start, length - start), + bases[dotCount], ipv4, upperBounds[dotCount])); + if (NS_FAILED(res)) { + return NS_ERROR_FAILURE; + } + + // parse remaining parts starting from first part + int32_t lastUsed = -1; + for (int32_t i = 0; i < dotCount; i++) { + uint32_t number; + start = lastUsed + 1; + lastUsed = dotIndex[i]; + res = + (onlyBase10 ? ParseIPv4Number10( + Substring(host, start, lastUsed - start), number, 255) + : ParseIPv4Number(Substring(host, start, lastUsed - start), + bases[i], number, 255)); + if (NS_FAILED(res)) { + return NS_ERROR_FAILURE; + } + ipv4 += number << (8 * (3 - i)); + } + + // A special case for ipv4 URL like "127." should have the same result as + // "127". + if (dotCount == 1 && dotIndex[0] == length - 1) { + ipv4 = (ipv4 & 0xff000000) >> 24; + } + + uint8_t ipSegments[4]; + NetworkEndian::writeUint32(ipSegments, ipv4); + result = nsPrintfCString("%d.%d.%d.%d", ipSegments[0], ipSegments[1], + ipSegments[2], ipSegments[3]); + return NS_OK; +} + +// Return the number of "dots" in the string, or -1 if invalid. Note that the +// number of relevant entries in the bases/starts/ends arrays is number of +// dots + 1. +// +// length is assumed to be <= host.Length(); the caller is responsible for that +// +// Note that the value returned is guaranteed to be in [-1, 3] range. +int32_t ValidateIPv4Number(const nsACString& host, int32_t bases[4], + int32_t dotIndex[3], bool& onlyBase10, + int32_t length, bool trailingDot) { + MOZ_ASSERT(length <= (int32_t)host.Length()); + if (length <= 0) { + return -1; + } + + bool lastWasNumber = false; // We count on this being false for i == 0 + int32_t dotCount = 0; + onlyBase10 = true; + + for (int32_t i = 0; i < length; i++) { + char current = host[i]; + if (current == '.') { + // A dot should not follow a dot, or be first - it can follow an x though. + if (!(lastWasNumber || + (i >= 2 && (host[i - 1] == 'X' || host[i - 1] == 'x') && + host[i - 2] == '0')) || + (i == (length - 1) && trailingDot)) { + return -1; + } + + if (dotCount > 2) { + return -1; + } + lastWasNumber = false; + dotIndex[dotCount] = i; + dotCount++; + } else if (current == 'X' || current == 'x') { + if (!lastWasNumber || // An X should not follow an X or a dot or be first + i == (length - 1) || // No trailing Xs allowed + (dotCount == 0 && + i != 1) || // If we had no dots, an X should be second + host[i - 1] != '0' || // X should always follow a 0. Guaranteed i > + // 0 as lastWasNumber is true + (dotCount > 0 && + host[i - 2] != '.')) { // And that zero follows a dot if it exists + return -1; + } + lastWasNumber = false; + bases[dotCount] = 16; + onlyBase10 = false; + + } else if (current == '0') { + if (i < length - 1 && // Trailing zero doesn't signal octal + host[i + 1] != '.' && // Lone zero is not octal + (i == 0 || host[i - 1] == '.')) { // Zero at start or following a dot + // is a candidate for octal + bases[dotCount] = 8; // This will turn to 16 above if X shows up + onlyBase10 = false; + } + lastWasNumber = true; + + } else if (current >= '1' && current <= '7') { + lastWasNumber = true; + + } else if (current >= '8' && current <= '9') { + if (bases[dotCount] == 8) { + return -1; + } + lastWasNumber = true; + + } else if ((current >= 'a' && current <= 'f') || + (current >= 'A' && current <= 'F')) { + if (bases[dotCount] != 16) { + return -1; + } + lastWasNumber = true; + + } else { + return -1; + } + } + + return dotCount; +} + +bool ContainsOnlyAsciiDigits(const nsDependentCSubstring& input) { + for (const auto* c = input.BeginReading(); c < input.EndReading(); c++) { + if (!IsAsciiDigit(*c)) { + return false; + } + } + + return true; +} + +bool ContainsOnlyAsciiHexDigits(const nsDependentCSubstring& input) { + for (const auto* c = input.BeginReading(); c < input.EndReading(); c++) { + if (!IsAsciiHexDigit(*c)) { + return false; + } + } + return true; +} + +} // namespace mozilla::net::IPv4Parser diff --git a/netwerk/base/IPv4Parser.h b/netwerk/base/IPv4Parser.h new file mode 100644 index 000000000000..0325d07d2eed --- /dev/null +++ b/netwerk/base/IPv4Parser.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_net_IPv4Parser_h +#define mozilla_net_IPv4Parser_h + +#include "nsString.h" + +namespace mozilla::net::IPv4Parser { + +bool EndsInANumber(const nsCString& input); +nsresult NormalizeIPv4(const nsACString& host, nsCString& result); + +nsresult ParseIPv4Number(const nsACString& input, int32_t base, + uint32_t& number, uint32_t maxNumber); +int32_t ValidateIPv4Number(const nsACString& host, int32_t bases[4], + int32_t dotIndex[3], bool& onlyBase10, + int32_t length, bool trailingDot); + +bool ContainsOnlyAsciiDigits(const nsDependentCSubstring& input); +bool ContainsOnlyAsciiHexDigits(const nsDependentCSubstring& input); +nsresult ParseIPv4Number10(const nsACString& input, uint32_t& number, + uint32_t maxNumber); +} // namespace mozilla::net::IPv4Parser + +#endif // mozilla_net_IPv4Parser_h diff --git a/netwerk/base/moz.build b/netwerk/base/moz.build index dc7da96fd47d..d0fc981f7eb2 100644 --- a/netwerk/base/moz.build +++ b/netwerk/base/moz.build @@ -163,6 +163,7 @@ EXPORTS.mozilla.net += [ "DashboardTypes.h", "DefaultURI.h", "InterceptionInfo.h", + "IPv4Parser.h", "MemoryDownloader.h", "NetworkConnectivityService.h", "Predictor.h", @@ -184,6 +185,7 @@ UNIFIED_SOURCES += [ "DefaultURI.cpp", "EventTokenBucket.cpp", "InterceptionInfo.cpp", + "IPv4Parser.cpp", "LoadContextInfo.cpp", "LoadInfo.cpp", "MemoryDownloader.cpp", diff --git a/netwerk/base/nsIIOService.idl b/netwerk/base/nsIIOService.idl index 9a28aa864c40..bbf0212f048a 100644 --- a/netwerk/base/nsIIOService.idl +++ b/netwerk/base/nsIIOService.idl @@ -237,7 +237,10 @@ interface nsIIOService : nsISupports boolean hostnameIsSharedIPAddress(in nsIURI aURI); /** - * Checks if a hostname is valid. + * Checks if characters not allowed in DNS are present in the hostname + * and if the hostname ends in a number it also checks if it's a valid + * IPv4 address. Any failure indicates that parsing this host will fail at a + * later point when using it in the URL parser. * * @param AUTF8String hostname is the hostname to validate * @return true if the hostname is valid, else false diff --git a/netwerk/base/nsIOService.cpp b/netwerk/base/nsIOService.cpp index bec236fd2a9f..87ab92645247 100644 --- a/netwerk/base/nsIOService.cpp +++ b/netwerk/base/nsIOService.cpp @@ -67,6 +67,7 @@ #include "mozilla/StaticPrefs_security.h" #include "mozilla/glean/GleanMetrics.h" #include "nsNSSComponent.h" +#include "IPv4Parser.h" #include "ssl.h" #include "StaticComponents.h" @@ -1014,8 +1015,29 @@ nsIOService::HostnameIsSharedIPAddress(nsIURI* aURI, bool* aResult) { NS_IMETHODIMP nsIOService::IsValidHostname(const nsACString& inHostname, bool* aResult) { - *aResult = net_IsValidHostName(inHostname); + if (!net_IsValidDNSHost(inHostname)) { + *aResult = false; + return NS_OK; + } + // hostname ending with a "." delimited octet that is a number + // must be IPv4 or IPv6 dual address + nsAutoCString host(inHostname); + if (IPv4Parser::EndsInANumber(host)) { + // ipv6 dual address; for example "::1.2.3.4" + if (net_IsValidIPv6Addr(host)) { + *aResult = true; + return NS_OK; + } + + nsAutoCString normalized; + nsresult rv = IPv4Parser::NormalizeIPv4(host, normalized); + if (NS_FAILED(rv)) { + *aResult = false; + return NS_OK; + } + } + *aResult = true; return NS_OK; } diff --git a/netwerk/base/nsSocketTransport2.cpp b/netwerk/base/nsSocketTransport2.cpp index d4c2b253e183..80f688310190 100644 --- a/netwerk/base/nsSocketTransport2.cpp +++ b/netwerk/base/nsSocketTransport2.cpp @@ -962,7 +962,7 @@ nsresult nsSocketTransport::ResolveHost() { // When not resolving mHost locally, we still want to ensure that // it only contains valid characters. See bug 304904 for details. // Sometimes the end host is not yet known and mHost is * - if (!net_IsValidHostName(mHost) && !mHost.EqualsLiteral("*")) { + if (!net_IsValidDNSHost(mHost) && !mHost.EqualsLiteral("*")) { SOCKET_LOG((" invalid hostname %s\n", mHost.get())); return NS_ERROR_UNKNOWN_HOST; } diff --git a/netwerk/base/nsStandardURL.cpp b/netwerk/base/nsStandardURL.cpp index 7e868ef2919a..78a5603f6feb 100644 --- a/netwerk/base/nsStandardURL.cpp +++ b/netwerk/base/nsStandardURL.cpp @@ -28,10 +28,10 @@ #include "prprf.h" #include "nsReadableUtils.h" #include "mozilla/net/MozURL_ffi.h" -#include "mozilla/TextUtils.h" #include "mozilla/Utf8.h" #include "nsIClassInfoImpl.h" #include +#include "IPv4Parser.h" // // setenv MOZ_LOG nsStandardURL:5 @@ -402,228 +402,6 @@ void nsStandardURL::InvalidateCache(bool invalidateCachedFile) { } } -// Return the number of "dots" in the string, or -1 if invalid. Note that the -// number of relevant entries in the bases/starts/ends arrays is number of -// dots + 1. -// -// length is assumed to be <= host.Length(); the caller is responsible for that -// -// Note that the value returned is guaranteed to be in [-1, 3] range. -inline int32_t ValidateIPv4Number(const nsACString& host, int32_t bases[4], - int32_t dotIndex[3], bool& onlyBase10, - int32_t length, bool trailingDot) { - MOZ_ASSERT(length <= (int32_t)host.Length()); - if (length <= 0) { - return -1; - } - - bool lastWasNumber = false; // We count on this being false for i == 0 - int32_t dotCount = 0; - onlyBase10 = true; - - for (int32_t i = 0; i < length; i++) { - char current = host[i]; - if (current == '.') { - // A dot should not follow a dot, or be first - it can follow an x though. - if (!(lastWasNumber || - (i >= 2 && (host[i - 1] == 'X' || host[i - 1] == 'x') && - host[i - 2] == '0')) || - (i == (length - 1) && trailingDot)) { - return -1; - } - - if (dotCount > 2) { - return -1; - } - lastWasNumber = false; - dotIndex[dotCount] = i; - dotCount++; - } else if (current == 'X' || current == 'x') { - if (!lastWasNumber || // An X should not follow an X or a dot or be first - i == (length - 1) || // No trailing Xs allowed - (dotCount == 0 && - i != 1) || // If we had no dots, an X should be second - host[i - 1] != '0' || // X should always follow a 0. Guaranteed i > - // 0 as lastWasNumber is true - (dotCount > 0 && - host[i - 2] != '.')) { // And that zero follows a dot if it exists - return -1; - } - lastWasNumber = false; - bases[dotCount] = 16; - onlyBase10 = false; - - } else if (current == '0') { - if (i < length - 1 && // Trailing zero doesn't signal octal - host[i + 1] != '.' && // Lone zero is not octal - (i == 0 || host[i - 1] == '.')) { // Zero at start or following a dot - // is a candidate for octal - bases[dotCount] = 8; // This will turn to 16 above if X shows up - onlyBase10 = false; - } - lastWasNumber = true; - - } else if (current >= '1' && current <= '7') { - lastWasNumber = true; - - } else if (current >= '8' && current <= '9') { - if (bases[dotCount] == 8) { - return -1; - } - lastWasNumber = true; - - } else if ((current >= 'a' && current <= 'f') || - (current >= 'A' && current <= 'F')) { - if (bases[dotCount] != 16) { - return -1; - } - lastWasNumber = true; - - } else { - return -1; - } - } - - return dotCount; -} - -inline nsresult ParseIPv4Number10(const nsACString& input, uint32_t& number, - uint32_t maxNumber) { - uint64_t value = 0; - const char* current = input.BeginReading(); - const char* end = input.EndReading(); - for (; current < end; ++current) { - char c = *current; - MOZ_ASSERT(c >= '0' && c <= '9'); - value *= 10; - value += c - '0'; - } - if (value <= maxNumber) { - number = value; - return NS_OK; - } - - // The error case - number = 0; - return NS_ERROR_FAILURE; -} - -inline nsresult ParseIPv4Number(const nsACString& input, int32_t base, - uint32_t& number, uint32_t maxNumber) { - // Accumulate in the 64-bit value - uint64_t value = 0; - const char* current = input.BeginReading(); - const char* end = input.EndReading(); - switch (base) { - case 16: - ++current; - [[fallthrough]]; - case 8: - ++current; - break; - case 10: - default: - break; - } - for (; current < end; ++current) { - value *= base; - char c = *current; - MOZ_ASSERT((base == 10 && IsAsciiDigit(c)) || - (base == 8 && c >= '0' && c <= '7') || - (base == 16 && IsAsciiHexDigit(c))); - if (IsAsciiDigit(c)) { - value += c - '0'; - } else if (c >= 'a' && c <= 'f') { - value += c - 'a' + 10; - } else if (c >= 'A' && c <= 'F') { - value += c - 'A' + 10; - } - } - - if (value <= maxNumber) { - number = value; - return NS_OK; - } - - // The error case - number = 0; - return NS_ERROR_FAILURE; -} - -// IPv4 parser spec: https://url.spec.whatwg.org/#concept-ipv4-parser -/* static */ -nsresult nsStandardURL::NormalizeIPv4(const nsACString& host, - nsCString& result) { - int32_t bases[4] = {10, 10, 10, 10}; - bool onlyBase10 = true; // Track this as a special case - int32_t dotIndex[3]; // The positions of the dots in the string - - // Use "length" rather than host.Length() after call to - // ValidateIPv4Number because of potential trailing period. - nsDependentCSubstring filteredHost; - bool trailingDot = false; - if (host.Length() > 0 && host.Last() == '.') { - trailingDot = true; - filteredHost.Rebind(host.BeginReading(), host.Length() - 1); - } else { - filteredHost.Rebind(host.BeginReading(), host.Length()); - } - - int32_t length = static_cast(filteredHost.Length()); - int32_t dotCount = ValidateIPv4Number(filteredHost, bases, dotIndex, - onlyBase10, length, trailingDot); - if (dotCount < 0 || length <= 0) { - return NS_ERROR_FAILURE; - } - - // Max values specified by the spec - static const uint32_t upperBounds[] = {0xffffffffu, 0xffffffu, 0xffffu, - 0xffu}; - uint32_t ipv4; - int32_t start = (dotCount > 0 ? dotIndex[dotCount - 1] + 1 : 0); - - // parse the last part first - nsresult res; - // Doing a special case for all items being base 10 gives ~35% speedup - res = (onlyBase10 - ? ParseIPv4Number10(Substring(host, start, length - start), ipv4, - upperBounds[dotCount]) - : ParseIPv4Number(Substring(host, start, length - start), - bases[dotCount], ipv4, upperBounds[dotCount])); - if (NS_FAILED(res)) { - return NS_ERROR_FAILURE; - } - - // parse remaining parts starting from first part - int32_t lastUsed = -1; - for (int32_t i = 0; i < dotCount; i++) { - uint32_t number; - start = lastUsed + 1; - lastUsed = dotIndex[i]; - res = - (onlyBase10 ? ParseIPv4Number10( - Substring(host, start, lastUsed - start), number, 255) - : ParseIPv4Number(Substring(host, start, lastUsed - start), - bases[i], number, 255)); - if (NS_FAILED(res)) { - return NS_ERROR_FAILURE; - } - ipv4 += number << (8 * (3 - i)); - } - - // A special case for ipv4 URL like "127." should have the same result as - // "127". - if (dotCount == 1 && dotIndex[0] == length - 1) { - ipv4 = (ipv4 & 0xff000000) >> 24; - } - - uint8_t ipSegments[4]; - NetworkEndian::writeUint32(ipSegments, ipv4); - result = nsPrintfCString("%d.%d.%d.%d", ipSegments[0], ipSegments[1], - ipSegments[2], ipSegments[3]); - return NS_OK; -} - nsIIDNService* nsStandardURL::GetIDNService() { return gIDN.get(); } nsresult nsStandardURL::NormalizeIDN(const nsACString& aHost, @@ -712,71 +490,6 @@ uint32_t nsStandardURL::AppendToBuf(char* buf, uint32_t i, const char* str, return i + len; } -static bool ContainsOnlyAsciiDigits(const nsDependentCSubstring& input) { - for (const auto* c = input.BeginReading(); c < input.EndReading(); c++) { - if (!IsAsciiDigit(*c)) { - return false; - } - } - - return true; -} - -static bool ContainsOnlyAsciiHexDigits(const nsDependentCSubstring& input) { - for (const auto* c = input.BeginReading(); c < input.EndReading(); c++) { - if (!IsAsciiHexDigit(*c)) { - return false; - } - } - return true; -} - -// https://url.spec.whatwg.org/#ends-in-a-number-checker -static bool EndsInANumber(const nsCString& input) { - // 1. Let parts be the result of strictly splitting input on U+002E (.). - nsTArray parts; - for (const nsDependentCSubstring& part : input.Split('.')) { - parts.AppendElement(part); - } - - if (parts.Length() == 0) { - return false; - } - - // 2.If the last item in parts is the empty string, then: - // 1. If parts’s size is 1, then return false. - // 2. Remove the last item from parts. - if (parts.LastElement().IsEmpty()) { - if (parts.Length() == 1) { - return false; - } - Unused << parts.PopLastElement(); - } - - // 3. Let last be the last item in parts. - const nsDependentCSubstring& last = parts.LastElement(); - - // 4. If last is non-empty and contains only ASCII digits, then return true. - // The erroneous input "09" will be caught by the IPv4 parser at a later - // stage. - if (!last.IsEmpty()) { - if (ContainsOnlyAsciiDigits(last)) { - return true; - } - } - - // 5. If parsing last as an IPv4 number does not return failure, then return - // true. This is equivalent to checking that last is "0X" or "0x", followed by - // zero or more ASCII hex digits. - if (StringBeginsWith(last, "0x"_ns) || StringBeginsWith(last, "0X"_ns)) { - if (ContainsOnlyAsciiHexDigits(Substring(last, 2))) { - return true; - } - } - - return false; -} - // basic algorithm: // 1- escape url segments (for improved GetSpec efficiency) // 2- allocate spec buffer @@ -879,9 +592,9 @@ nsresult nsStandardURL::BuildNormalizedSpec(const char* spec, if (NS_FAILED(rv)) { return rv; } - if (EndsInANumber(encHost) && allowIp) { + if (IPv4Parser::EndsInANumber(encHost) && allowIp) { nsAutoCString ipString; - rv = NormalizeIPv4(encHost, ipString); + rv = IPv4Parser::NormalizeIPv4(encHost, ipString); if (NS_FAILED(rv)) { return rv; } @@ -2191,9 +1904,9 @@ nsresult nsStandardURL::SetHost(const nsACString& input) { if (NS_FAILED(rv)) { return rv; } - if (EndsInANumber(hostBuf) && allowIp) { + if (IPv4Parser::EndsInANumber(hostBuf) && allowIp) { nsAutoCString ipString; - rv = NormalizeIPv4(hostBuf, ipString); + rv = IPv4Parser::NormalizeIPv4(hostBuf, ipString); if (NS_FAILED(rv)) { return rv; } @@ -3980,21 +3693,3 @@ size_t nsStandardURL::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { } // namespace net } // namespace mozilla - -// For unit tests. Including nsStandardURL.h seems to cause problems -nsresult Test_NormalizeIPv4(const nsACString& host, nsCString& result) { - return mozilla::net::nsStandardURL::NormalizeIPv4(host, result); -} - -// For unit tests. Including nsStandardURL.h seems to cause problems -nsresult Test_ParseIPv4Number(const nsACString& input, int32_t base, - uint32_t& number, uint32_t maxNumber) { - return mozilla::net::ParseIPv4Number(input, base, number, maxNumber); -} - -int32_t Test_ValidateIPv4Number(const nsACString& host, int32_t bases[4], - int32_t dotIndex[3], bool& onlyBase10, - int32_t length) { - return mozilla::net::ValidateIPv4Number(host, bases, dotIndex, onlyBase10, - length, false); -} diff --git a/netwerk/base/nsStandardURL.h b/netwerk/base/nsStandardURL.h index 4f32fd9a7729..16adce2758b1 100644 --- a/netwerk/base/nsStandardURL.h +++ b/netwerk/base/nsStandardURL.h @@ -196,8 +196,6 @@ class nsStandardURL : public nsIFileURL, }; friend class nsSegmentEncoder; - static nsresult NormalizeIPv4(const nsACString& host, nsCString& result); - static nsIIDNService* GetIDNService(); protected: diff --git a/netwerk/base/nsURLHelper.cpp b/netwerk/base/nsURLHelper.cpp index 16bca4b92be9..2065f094ddc0 100644 --- a/netwerk/base/nsURLHelper.cpp +++ b/netwerk/base/nsURLHelper.cpp @@ -918,7 +918,7 @@ void net_ParseRequestContentType(const nsACString& aHeaderStr, *aHadCharset = hadCharset; } -bool net_IsValidHostName(const nsACString& host) { +bool net_IsValidDNSHost(const nsACString& host) { // The host name is limited to 253 ascii characters. if (host.Length() > 253) { return false; diff --git a/netwerk/base/nsURLHelper.h b/netwerk/base/nsURLHelper.h index 4c6a896cb2c7..84e25f0314b8 100644 --- a/netwerk/base/nsURLHelper.h +++ b/netwerk/base/nsURLHelper.h @@ -219,7 +219,7 @@ inline char* net_RFindCharNotInSet(const char* str, const char* set) { * This function returns true if the given hostname does not include any * restricted characters. Otherwise, false is returned. */ -bool net_IsValidHostName(const nsACString& host); +bool net_IsValidDNSHost(const nsACString& host); /** * Checks whether the IPv4 address is valid according to RFC 3986 section 3.2.2. diff --git a/netwerk/dns/nsHostResolver.cpp b/netwerk/dns/nsHostResolver.cpp index d343f84a04cd..bfb58e9d6184 100644 --- a/netwerk/dns/nsHostResolver.cpp +++ b/netwerk/dns/nsHostResolver.cpp @@ -524,7 +524,7 @@ nsresult nsHostResolver::ResolveHost(const nsACString& aHost, // ensure that we are working with a valid hostname before proceeding. see // bug 304904 for details. - if (!net_IsValidHostName(host)) { + if (!net_IsValidDNSHost(host)) { return NS_ERROR_UNKNOWN_HOST; } diff --git a/netwerk/protocol/http/TRRServiceChannel.cpp b/netwerk/protocol/http/TRRServiceChannel.cpp index 3a7a6efaf30e..4bb16d4d857b 100644 --- a/netwerk/protocol/http/TRRServiceChannel.cpp +++ b/netwerk/protocol/http/TRRServiceChannel.cpp @@ -489,7 +489,7 @@ nsresult TRRServiceChannel::ContinueOnBeforeConnect() { LOG(("TRRServiceChannel::ContinueOnBeforeConnect [this=%p]\n", this)); // ensure that we are using a valid hostname - if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin()))) { + if (!net_IsValidDNSHost(nsDependentCString(mConnectionInfo->Origin()))) { return NS_ERROR_UNKNOWN_HOST; } diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index 56046799efa2..b968ed0943eb 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -883,7 +883,7 @@ nsresult nsHttpChannel::ContinueOnBeforeConnect(bool aShouldUpgrade, } // ensure that we are using a valid hostname - if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin()))) { + if (!net_IsValidDNSHost(nsDependentCString(mConnectionInfo->Origin()))) { return NS_ERROR_UNKNOWN_HOST; } diff --git a/netwerk/test/gtest/TestStandardURL.cpp b/netwerk/test/gtest/TestStandardURL.cpp index afefead8ef4d..d99d2257ca42 100644 --- a/netwerk/test/gtest/TestStandardURL.cpp +++ b/netwerk/test/gtest/TestStandardURL.cpp @@ -15,16 +15,14 @@ #include "mozilla/Base64.h" #include "nsEscape.h" #include "nsURLHelper.h" +#include "IPv4Parser.h" using namespace mozilla; -// In nsStandardURL.cpp -extern nsresult Test_NormalizeIPv4(const nsACString& host, nsCString& result); -extern nsresult Test_ParseIPv4Number(const nsACString& input, int32_t base, - uint32_t& number, uint32_t maxNumber); -extern int32_t Test_ValidateIPv4Number(const nsACString& host, int32_t bases[4], - int32_t dotIndex[3], bool& onlyBase10, - int32_t length); +nsresult Test_NormalizeIPv4(const nsACString& host, nsCString& result) { + return net::IPv4Parser::NormalizeIPv4(host, result); +} + TEST(TestStandardURL, Simple) { nsCOMPtr url; @@ -430,14 +428,14 @@ TEST(TestStandardURL, ParseIPv4Num) int32_t dotIndex[3]; // The positions of the dots in the string int32_t length = static_cast(host.Length()); - ASSERT_EQ(2, - Test_ValidateIPv4Number(host, bases, dotIndex, onlyBase10, length)); + ASSERT_EQ(2, mozilla::net::IPv4Parser::ValidateIPv4Number( + host, bases, dotIndex, onlyBase10, length, false)); nsCString result; ASSERT_EQ(NS_OK, Test_NormalizeIPv4("0x.0x.0"_ns, result)); uint32_t number; - Test_ParseIPv4Number("0x10"_ns, 16, number, 255); + mozilla::net::IPv4Parser::ParseIPv4Number("0x10"_ns, 16, number, 255); ASSERT_EQ(number, (uint32_t)16); } diff --git a/netwerk/test/unit/test_standardurl.js b/netwerk/test/unit/test_standardurl.js index 7dd9a6f7ff7b..4d02fb868307 100644 --- a/netwerk/test/unit/test_standardurl.js +++ b/netwerk/test/unit/test_standardurl.js @@ -1189,3 +1189,19 @@ add_task(async function test_bug1890346() { let url = Services.io.newURI("file:..?/.."); equal(url.spec, "file:///?/.."); }); + +add_task(async function test_bug1914141() { + equal(Services.io.isValidHostname("example.com"), true); + equal(Services.io.isValidHostname("example.0"), false); + + equal(Services.io.isValidHostname("192.168.0.1"), true); + equal(Services.io.isValidHostname("192.168.0"), true); + equal(Services.io.isValidHostname("1.192.168.0.1"), false); + equal(Services.io.isValidHostname("invalid.192.168.0.1"), false); + + equal(Services.io.isValidHostname("::1"), true); + equal(Services.io.isValidHostname("abcd::zz::00"), false); + equal(Services.io.isValidHostname("zzzz::1.2.3.4"), false); + + equal(Services.io.isValidHostname("::1.2.3.4"), true); +});