/* -*- 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 "CookieValidation.h" #include "CookieLogging.h" #include "CookieService.h" #include "CookiePrefixes.h" #include "mozilla/dom/nsMixedContentBlocker.h" #include "mozilla/StaticPrefs_network.h" constexpr uint32_t kMaxBytesPerCookie = 4096; constexpr uint32_t kMaxBytesPerDomain = 1024; constexpr uint32_t kMaxBytesPerPath = 1024; using namespace mozilla::net; NS_IMPL_ISUPPORTS(CookieValidation, nsICookieValidation) CookieValidation::CookieValidation(const CookieStruct& aCookieData) : mCookieData(aCookieData) {} // static already_AddRefed CookieValidation::Validate( const CookieStruct& aCookieData) { RefPtr cv = new CookieValidation(aCookieData); cv->ValidateInternal(); return cv.forget(); } // static already_AddRefed CookieValidation::ValidateForHost( const CookieStruct& aCookieData, nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp) { RefPtr cv = new CookieValidation(aCookieData); cv->ValidateForHostInternal(aHostURI, aBaseDomain, aRequireHostMatch, aFromHttp); return cv.forget(); } // static already_AddRefed CookieValidation::ValidateInContext( const CookieStruct& aCookieData, nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp, bool aIsForeignAndNotAddon, bool aPartitionedOnly, bool aIsInPrivateBrowsing) { RefPtr cv = new CookieValidation(aCookieData); cv->ValidateInContextInternal(aHostURI, aBaseDomain, aRequireHostMatch, aFromHttp, aIsForeignAndNotAddon, aPartitionedOnly, aIsInPrivateBrowsing); return cv.forget(); } void CookieValidation::ValidateInternal() { MOZ_ASSERT(mResult == eOK); // reject cookie if name and value are empty, per RFC6265bis if (mCookieData.name().IsEmpty() && mCookieData.value().IsEmpty()) { mResult = eRejectedEmptyNameAndValue; return; } // reject cookie if it's over the size limit, per RFC2109 if (!CheckNameAndValueSize(mCookieData)) { mResult = eRejectedNameValueOversize; return; } if (!CheckName(mCookieData)) { mResult = eRejectedInvalidCharName; return; } if (!CheckValue(mCookieData)) { mResult = eRejectedInvalidCharValue; return; } if (mCookieData.path().Length() > kMaxBytesPerPath) { mResult = eRejectedAttributePathOversize; return; } if (mCookieData.path().Contains('\t')) { mResult = eRejectedInvalidPath; return; } if (mCookieData.host().Length() > kMaxBytesPerDomain) { mResult = eRejectedAttributeDomainOversize; return; } // If a cookie is nameless, then its value must not start with a known prefix. if (mCookieData.name().IsEmpty() && CookiePrefixes::Has(mCookieData.value())) { mResult = eRejectedInvalidPrefix; return; } // If same-site is explicitly set to 'none' but this is not a secure context, // let's abort the parsing. if (!mCookieData.isSecure() && mCookieData.sameSite() == nsICookie::SAMESITE_NONE) { if (StaticPrefs::network_cookie_sameSite_noneRequiresSecure()) { mResult = eRejectedNoneRequiresSecure; return; } // Still warn about the missing Secure attribute when not enforcing. mWarnings.mSameSiteNoneRequiresSecureForBeta = true; } // This part checks if the caleers have set the expiry value to max 400 days. if (!mCookieData.isSession()) { int64_t maxageCap = StaticPrefs::network_cookie_maxageCap(); int64_t creationTimeInMSec = mCookieData.updateTimeInUSec() / PR_USEC_PER_MSEC; int64_t expiryInMSec = mCookieData.expiryInMSec(); if (maxageCap && expiryInMSec > creationTimeInMSec + maxageCap * 1000) { mResult = eRejectedAttributeExpiryOversize; return; } } } void CookieValidation::ValidateForHostInternal(nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp) { MOZ_ASSERT(mResult == eOK); ValidateInternal(); if (mResult != eOK) { return; } if (!aBaseDomain.IsEmpty() && !CheckDomain(mCookieData, aHostURI, aBaseDomain, aRequireHostMatch)) { mResult = eRejectedInvalidDomain; return; } // if the new cookie is httponly, make sure we're not coming from script if (!aFromHttp && mCookieData.isHttpOnly()) { mResult = eRejectedHttpOnlyButFromScript; return; } bool potentiallyTrustworthy = nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI); // FixDomain() and FixPath() from CookieParser MUST be run first to make sure // invalid attributes are rejected and to regularlize them. In particular all // explicit domain attributes result in a host that starts with a dot, and if // the host doesn't start with a dot it correctly matches the true // host. if (!CookiePrefixes::Check(mCookieData, potentiallyTrustworthy)) { mResult = eRejectedInvalidPrefix; return; } // If the new cookie is non-https and wants to set secure flag, // browser have to ignore this new cookie. // (draft-ietf-httpbis-cookie-alone section 3.1) if (mCookieData.isSecure() && !potentiallyTrustworthy) { mResult = eRejectedSecureButNonHttps; return; } if (mCookieData.sameSite() == nsICookie::SAMESITE_UNSET) { bool laxByDefault = StaticPrefs::network_cookie_sameSite_laxByDefault() && !nsContentUtils::IsURIInPrefList( aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); if (laxByDefault) { mWarnings.mSameSiteLaxForced = true; } else if (StaticPrefs:: network_cookie_sameSite_laxByDefaultWarningsForBeta()) { mWarnings.mSameSiteLaxForcedForBeta = true; } } } void CookieValidation::ValidateInContextInternal( nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp, bool aIsForeignAndNotAddon, bool aPartitionedOnly, bool aIsInPrivateBrowsing) { MOZ_ASSERT(mResult == eOK); ValidateForHostInternal(aHostURI, aBaseDomain, aRequireHostMatch, aFromHttp); if (mResult != eOK) { return; } // If the cookie is same-site but in a cross site context, browser must // ignore the cookie. bool laxByDefault = StaticPrefs::network_cookie_sameSite_laxByDefault() && !nsContentUtils::IsURIInPrefList( aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); uint32_t sameSite = mCookieData.sameSite(); if (sameSite == nsICookie::SAMESITE_UNSET) { sameSite = laxByDefault ? nsICookie::SAMESITE_LAX : nsICookie::SAMESITE_NONE; } if (sameSite != nsICookie::SAMESITE_NONE && aIsForeignAndNotAddon) { mResult = eRejectedForNonSameSiteness; return; } // Ensure the partitioned cookie is set with the secure attribute if CHIPS // is enabled. This check should be part of ValidateInternal but it's not // because of bug 1965880. if (StaticPrefs::network_cookie_CHIPS_enabled() && mCookieData.isPartitioned() && !mCookieData.isSecure()) { mResult = eRejectedPartitionedRequiresSecure; return; } } NS_IMETHODIMP CookieValidation::GetResult(nsICookieValidation::ValidationError* aRetval) { NS_ENSURE_ARG_POINTER(aRetval); *aRetval = mResult; return NS_OK; } // static bool CookieValidation::CheckDomain(const CookieStruct& aCookieData, nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch) { // Note: The logic in this function is mirrored in // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions(). // If it changes, please update that function, or file a bug for someone // else to do so. if (aCookieData.host().IsEmpty()) { return false; } // get host from aHostURI nsAutoCString hostFromURI; nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI); // check whether the host is either an IP address, an alias such as // 'localhost', an eTLD such as 'co.uk', or the empty string. in these // cases, require an exact string match for the domain, and leave the cookie // as a non-domain one. bug 105917 originally noted the requirement to deal // with IP addresses. if (aRequireHostMatch) { return hostFromURI.Equals(aCookieData.host()); } nsCString cookieHost = aCookieData.host(); // Tolerate leading '.' characters, but not if it's otherwise an empty host. if (aCookieData.host().Length() > 1 && aCookieData.host().First() == '.') { cookieHost.Cut(0, 1); } // ensure the proposed domain is derived from the base domain; and also // that the host domain is derived from the proposed domain (per RFC2109). if (CookieCommons::IsSubdomainOf(cookieHost, aBaseDomain) && CookieCommons::IsSubdomainOf(hostFromURI, cookieHost)) { return true; } /* * note: RFC2109 section 4.3.2 requires that we check the following: * that the portion of host not in domain does not contain a dot. * this prevents hosts of the form x.y.co.nz from setting cookies in the * entire .co.nz domain. however, it's only a only a partial solution and * it breaks sites (IE doesn't enforce it), so we don't perform this check. */ return false; } void CookieValidation::RetrieveErrorLogData(uint32_t* aFlags, nsACString& aCategory, nsACString& aKey, nsTArray& aParams) const { MOZ_ASSERT(aFlags); MOZ_ASSERT(aParams.IsEmpty()); *aFlags = nsIScriptError::errorFlag; #define SET_LOG_DATA(category, x) \ aCategory = category; \ aKey = x; \ aParams.AppendElement(NS_ConvertUTF8toUTF16(mCookieData.name())); switch (mResult) { case eOK: return; case eRejectedEmptyNameAndValue: { *aFlags = nsIScriptError::warningFlag; aCategory.Assign(CONSOLE_REJECTION_CATEGORY); aKey.Assign("CookieRejectedEmptyNameAndValue"_ns); return; } case eRejectedNoneRequiresSecure: { SET_LOG_DATA(CONSOLE_SAMESITE_CATEGORY, "CookieRejectedNonRequiresSecure2"_ns); return; } case eRejectedPartitionedRequiresSecure: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedPartitionedRequiresSecure"_ns); return; } case eRejectedNameValueOversize: { *aFlags = nsIScriptError::warningFlag; aCategory.Assign(CONSOLE_OVERSIZE_CATEGORY); aKey.Assign("CookieOversize"_ns); aParams.AppendElement(NS_ConvertUTF8toUTF16(mCookieData.name())); nsString size; size.AppendInt(kMaxBytesPerCookie); aParams.AppendElement(size); return; } case eRejectedInvalidCharName: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidCharName"_ns); return; } case eRejectedInvalidCharValue: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidCharValue"_ns); return; } case eRejectedAttributePathOversize: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedAttributePathOversize"_ns); return; } case eRejectedAttributeDomainOversize: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedAttributeDomainOversize"_ns); return; } case eRejectedAttributeExpiryOversize: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedAttributeExpiryOversize"_ns); return; } case eRejectedInvalidPath: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidPath"_ns); return; } case eRejectedInvalidDomain: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidDomain"_ns); return; } case eRejectedInvalidPrefix: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidPrefix"_ns); return; } case eRejectedHttpOnlyButFromScript: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedHttpOnlyButFromScript"_ns); return; } case eRejectedSecureButNonHttps: { SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedSecureButNonHttps"_ns); return; } case eRejectedForNonSameSiteness: { SET_LOG_DATA(CONSOLE_SAMESITE_CATEGORY, "CookieRejectedForNonSameSiteness"_ns); return; } } #undef SET_LOG_DATA } void CookieValidation::ReportErrorsAndWarnings(nsIConsoleReportCollector* aCRC, nsIURI* aHostURI) const { if (mResult != eOK) { uint32_t flags; nsAutoCString category; nsAutoCString key; nsTArray params; RetrieveErrorLogData(&flags, category, key, params); CookieLogging::LogMessageToConsole(aCRC, aHostURI, flags, category, key, params); return; } if (mWarnings.mSameSiteNoneRequiresSecureForBeta) { CookieLogging::LogMessageToConsole( aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, "CookieRejectedNonRequiresSecureForBeta3"_ns, AutoTArray{NS_ConvertUTF8toUTF16(mCookieData.name()), SAMESITE_MDN_URL}); } if (mWarnings.mSameSiteLaxForced) { CookieLogging::LogMessageToConsole( aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY, "CookieLaxForced2"_ns, AutoTArray{NS_ConvertUTF8toUTF16(mCookieData.name())}); } if (mWarnings.mSameSiteLaxForcedForBeta) { CookieLogging::LogMessageToConsole( aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, "CookieLaxForcedForBeta2"_ns, AutoTArray{NS_ConvertUTF8toUTF16(mCookieData.name()), SAMESITE_MDN_URL}); } } NS_IMETHODIMP CookieValidation::GetErrorString(nsAString& aResult) { if (mResult == eOK) { return NS_OK; } uint32_t flags; nsAutoCString category; nsAutoCString key; nsTArray params; RetrieveErrorLogData(&flags, category, key, params); return nsContentUtils::FormatLocalizedString( nsContentUtils::eNECKO_PROPERTIES_en_US, key.get(), params, aResult); } // static bool CookieValidation::CheckNameAndValueSize(const CookieStruct& aCookieData) { // reject cookie if it's over the size limit, per RFC2109 return (aCookieData.name().Length() + aCookieData.value().Length()) <= kMaxBytesPerCookie; } bool CookieValidation::CheckName(const CookieStruct& aCookieData) { if (!aCookieData.name().IsEmpty() && (aCookieData.name().First() == 0x20 || aCookieData.name().Last() == 0x20)) { return false; } const char illegalNameCharacters[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x3D, 0x7F, 0x00}; const auto* start = aCookieData.name().BeginReading(); const auto* end = aCookieData.name().EndReading(); auto charFilter = [&](unsigned char c) { if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) { return true; } return std::find(std::begin(illegalNameCharacters), std::end(illegalNameCharacters), c) != std::end(illegalNameCharacters); }; return std::find_if(start, end, charFilter) == end; } bool CookieValidation::CheckValue(const CookieStruct& aCookieData) { if (!aCookieData.value().IsEmpty() && (aCookieData.value().First() == 0x20 || aCookieData.value().Last() == 0x20)) { return false; } // reject cookie if value contains an RFC 6265 disallowed character - see // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423 // NOTE: this is not the full set of characters disallowed by 6265 - notably // 0x09, 0x20, 0x22, 0x2C, and 0x5C are missing from this list. const char illegalCharacters[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00}; const auto* start = aCookieData.value().BeginReading(); const auto* end = aCookieData.value().EndReading(); bool shouldBlockEqualInNamelessCookie = aCookieData.name().IsEmpty() && StaticPrefs::network_cookie_block_nameless_with_equal_char(); auto charFilter = [&](unsigned char c) { if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) { return true; } if (c == '=' && shouldBlockEqualInNamelessCookie) { return true; } return std::find(std::begin(illegalCharacters), std::end(illegalCharacters), c) != std::end(illegalCharacters); }; return std::find_if(start, end, charFilter) == end; }