CookieValidation.cpp (17697B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "CookieValidation.h" 7 #include "CookieLogging.h" 8 #include "CookieService.h" 9 #include "CookiePrefixes.h" 10 #include "mozilla/dom/nsMixedContentBlocker.h" 11 #include "mozilla/StaticPrefs_network.h" 12 13 constexpr uint32_t kMaxBytesPerCookie = 4096; 14 constexpr uint32_t kMaxBytesPerDomain = 1024; 15 constexpr uint32_t kMaxBytesPerPath = 1024; 16 17 using namespace mozilla::net; 18 19 NS_IMPL_ISUPPORTS(CookieValidation, nsICookieValidation) 20 21 CookieValidation::CookieValidation(const CookieStruct& aCookieData) 22 : mCookieData(aCookieData) {} 23 24 // static 25 already_AddRefed<CookieValidation> CookieValidation::Validate( 26 const CookieStruct& aCookieData) { 27 RefPtr<CookieValidation> cv = new CookieValidation(aCookieData); 28 cv->ValidateInternal(); 29 return cv.forget(); 30 } 31 32 // static 33 already_AddRefed<CookieValidation> CookieValidation::ValidateForHost( 34 const CookieStruct& aCookieData, nsIURI* aHostURI, 35 const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp) { 36 RefPtr<CookieValidation> cv = new CookieValidation(aCookieData); 37 cv->ValidateForHostInternal(aHostURI, aBaseDomain, aRequireHostMatch, 38 aFromHttp); 39 return cv.forget(); 40 } 41 42 // static 43 already_AddRefed<CookieValidation> CookieValidation::ValidateInContext( 44 const CookieStruct& aCookieData, nsIURI* aHostURI, 45 const nsACString& aBaseDomain, bool aRequireHostMatch, bool aFromHttp, 46 bool aIsForeignAndNotAddon, bool aPartitionedOnly, 47 bool aIsInPrivateBrowsing) { 48 RefPtr<CookieValidation> cv = new CookieValidation(aCookieData); 49 cv->ValidateInContextInternal(aHostURI, aBaseDomain, aRequireHostMatch, 50 aFromHttp, aIsForeignAndNotAddon, 51 aPartitionedOnly, aIsInPrivateBrowsing); 52 return cv.forget(); 53 } 54 55 void CookieValidation::ValidateInternal() { 56 MOZ_ASSERT(mResult == eOK); 57 58 // reject cookie if name and value are empty, per RFC6265bis 59 if (mCookieData.name().IsEmpty() && mCookieData.value().IsEmpty()) { 60 mResult = eRejectedEmptyNameAndValue; 61 return; 62 } 63 64 // reject cookie if it's over the size limit, per RFC2109 65 if (!CheckNameAndValueSize(mCookieData)) { 66 mResult = eRejectedNameValueOversize; 67 return; 68 } 69 70 if (!CheckName(mCookieData)) { 71 mResult = eRejectedInvalidCharName; 72 return; 73 } 74 75 if (!CheckValue(mCookieData)) { 76 mResult = eRejectedInvalidCharValue; 77 return; 78 } 79 80 if (mCookieData.path().Length() > kMaxBytesPerPath) { 81 mResult = eRejectedAttributePathOversize; 82 return; 83 } 84 85 if (mCookieData.path().Contains('\t')) { 86 mResult = eRejectedInvalidPath; 87 return; 88 } 89 90 if (mCookieData.host().Length() > kMaxBytesPerDomain) { 91 mResult = eRejectedAttributeDomainOversize; 92 return; 93 } 94 95 // If a cookie is nameless, then its value must not start with a known prefix. 96 if (mCookieData.name().IsEmpty() && 97 CookiePrefixes::Has(mCookieData.value())) { 98 mResult = eRejectedInvalidPrefix; 99 return; 100 } 101 102 // If same-site is explicitly set to 'none' but this is not a secure context, 103 // let's abort the parsing. 104 if (!mCookieData.isSecure() && 105 mCookieData.sameSite() == nsICookie::SAMESITE_NONE) { 106 if (StaticPrefs::network_cookie_sameSite_noneRequiresSecure()) { 107 mResult = eRejectedNoneRequiresSecure; 108 return; 109 } 110 111 // Still warn about the missing Secure attribute when not enforcing. 112 mWarnings.mSameSiteNoneRequiresSecureForBeta = true; 113 } 114 115 // This part checks if the caleers have set the expiry value to max 400 days. 116 if (!mCookieData.isSession()) { 117 int64_t maxageCap = StaticPrefs::network_cookie_maxageCap(); 118 int64_t creationTimeInMSec = 119 mCookieData.updateTimeInUSec() / PR_USEC_PER_MSEC; 120 int64_t expiryInMSec = mCookieData.expiryInMSec(); 121 if (maxageCap && expiryInMSec > creationTimeInMSec + maxageCap * 1000) { 122 mResult = eRejectedAttributeExpiryOversize; 123 return; 124 } 125 } 126 } 127 128 void CookieValidation::ValidateForHostInternal(nsIURI* aHostURI, 129 const nsACString& aBaseDomain, 130 bool aRequireHostMatch, 131 bool aFromHttp) { 132 MOZ_ASSERT(mResult == eOK); 133 134 ValidateInternal(); 135 if (mResult != eOK) { 136 return; 137 } 138 139 if (!aBaseDomain.IsEmpty() && 140 !CheckDomain(mCookieData, aHostURI, aBaseDomain, aRequireHostMatch)) { 141 mResult = eRejectedInvalidDomain; 142 return; 143 } 144 145 // if the new cookie is httponly, make sure we're not coming from script 146 if (!aFromHttp && mCookieData.isHttpOnly()) { 147 mResult = eRejectedHttpOnlyButFromScript; 148 return; 149 } 150 151 bool potentiallyTrustworthy = 152 nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(aHostURI); 153 154 // FixDomain() and FixPath() from CookieParser MUST be run first to make sure 155 // invalid attributes are rejected and to regularlize them. In particular all 156 // explicit domain attributes result in a host that starts with a dot, and if 157 // the host doesn't start with a dot it correctly matches the true 158 // host. 159 if (!CookiePrefixes::Check(mCookieData, potentiallyTrustworthy)) { 160 mResult = eRejectedInvalidPrefix; 161 return; 162 } 163 164 // If the new cookie is non-https and wants to set secure flag, 165 // browser have to ignore this new cookie. 166 // (draft-ietf-httpbis-cookie-alone section 3.1) 167 if (mCookieData.isSecure() && !potentiallyTrustworthy) { 168 mResult = eRejectedSecureButNonHttps; 169 return; 170 } 171 172 if (mCookieData.sameSite() == nsICookie::SAMESITE_UNSET) { 173 bool laxByDefault = 174 StaticPrefs::network_cookie_sameSite_laxByDefault() && 175 !nsContentUtils::IsURIInPrefList( 176 aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); 177 if (laxByDefault) { 178 mWarnings.mSameSiteLaxForced = true; 179 } else if (StaticPrefs:: 180 network_cookie_sameSite_laxByDefaultWarningsForBeta()) { 181 mWarnings.mSameSiteLaxForcedForBeta = true; 182 } 183 } 184 } 185 186 void CookieValidation::ValidateInContextInternal( 187 nsIURI* aHostURI, const nsACString& aBaseDomain, bool aRequireHostMatch, 188 bool aFromHttp, bool aIsForeignAndNotAddon, bool aPartitionedOnly, 189 bool aIsInPrivateBrowsing) { 190 MOZ_ASSERT(mResult == eOK); 191 192 ValidateForHostInternal(aHostURI, aBaseDomain, aRequireHostMatch, aFromHttp); 193 if (mResult != eOK) { 194 return; 195 } 196 197 // If the cookie is same-site but in a cross site context, browser must 198 // ignore the cookie. 199 bool laxByDefault = 200 StaticPrefs::network_cookie_sameSite_laxByDefault() && 201 !nsContentUtils::IsURIInPrefList( 202 aHostURI, "network.cookie.sameSite.laxByDefault.disabledHosts"); 203 uint32_t sameSite = mCookieData.sameSite(); 204 if (sameSite == nsICookie::SAMESITE_UNSET) { 205 sameSite = 206 laxByDefault ? nsICookie::SAMESITE_LAX : nsICookie::SAMESITE_NONE; 207 } 208 209 if (sameSite != nsICookie::SAMESITE_NONE && aIsForeignAndNotAddon) { 210 mResult = eRejectedForNonSameSiteness; 211 return; 212 } 213 214 // Ensure the partitioned cookie is set with the secure attribute if CHIPS 215 // is enabled. This check should be part of ValidateInternal but it's not 216 // because of bug 1965880. 217 if (StaticPrefs::network_cookie_CHIPS_enabled() && 218 mCookieData.isPartitioned() && !mCookieData.isSecure()) { 219 mResult = eRejectedPartitionedRequiresSecure; 220 return; 221 } 222 } 223 224 NS_IMETHODIMP 225 CookieValidation::GetResult(nsICookieValidation::ValidationError* aRetval) { 226 NS_ENSURE_ARG_POINTER(aRetval); 227 *aRetval = mResult; 228 return NS_OK; 229 } 230 231 // static 232 bool CookieValidation::CheckDomain(const CookieStruct& aCookieData, 233 nsIURI* aHostURI, 234 const nsACString& aBaseDomain, 235 bool aRequireHostMatch) { 236 // Note: The logic in this function is mirrored in 237 // toolkit/components/extensions/ext-cookies.js:checkSetCookiePermissions(). 238 // If it changes, please update that function, or file a bug for someone 239 // else to do so. 240 241 if (aCookieData.host().IsEmpty()) { 242 return false; 243 } 244 245 // get host from aHostURI 246 nsAutoCString hostFromURI; 247 nsContentUtils::GetHostOrIPv6WithBrackets(aHostURI, hostFromURI); 248 249 // check whether the host is either an IP address, an alias such as 250 // 'localhost', an eTLD such as 'co.uk', or the empty string. in these 251 // cases, require an exact string match for the domain, and leave the cookie 252 // as a non-domain one. bug 105917 originally noted the requirement to deal 253 // with IP addresses. 254 if (aRequireHostMatch) { 255 return hostFromURI.Equals(aCookieData.host()); 256 } 257 258 nsCString cookieHost = aCookieData.host(); 259 // Tolerate leading '.' characters, but not if it's otherwise an empty host. 260 if (aCookieData.host().Length() > 1 && aCookieData.host().First() == '.') { 261 cookieHost.Cut(0, 1); 262 } 263 264 // ensure the proposed domain is derived from the base domain; and also 265 // that the host domain is derived from the proposed domain (per RFC2109). 266 if (CookieCommons::IsSubdomainOf(cookieHost, aBaseDomain) && 267 CookieCommons::IsSubdomainOf(hostFromURI, cookieHost)) { 268 return true; 269 } 270 271 /* 272 * note: RFC2109 section 4.3.2 requires that we check the following: 273 * that the portion of host not in domain does not contain a dot. 274 * this prevents hosts of the form x.y.co.nz from setting cookies in the 275 * entire .co.nz domain. however, it's only a only a partial solution and 276 * it breaks sites (IE doesn't enforce it), so we don't perform this check. 277 */ 278 return false; 279 } 280 281 void CookieValidation::RetrieveErrorLogData(uint32_t* aFlags, 282 nsACString& aCategory, 283 nsACString& aKey, 284 nsTArray<nsString>& aParams) const { 285 MOZ_ASSERT(aFlags); 286 MOZ_ASSERT(aParams.IsEmpty()); 287 288 *aFlags = nsIScriptError::errorFlag; 289 290 #define SET_LOG_DATA(category, x) \ 291 aCategory = category; \ 292 aKey = x; \ 293 aParams.AppendElement(NS_ConvertUTF8toUTF16(mCookieData.name())); 294 295 switch (mResult) { 296 case eOK: 297 return; 298 299 case eRejectedEmptyNameAndValue: { 300 *aFlags = nsIScriptError::warningFlag; 301 aCategory.Assign(CONSOLE_REJECTION_CATEGORY); 302 aKey.Assign("CookieRejectedEmptyNameAndValue"_ns); 303 return; 304 } 305 306 case eRejectedNoneRequiresSecure: { 307 SET_LOG_DATA(CONSOLE_SAMESITE_CATEGORY, 308 "CookieRejectedNonRequiresSecure2"_ns); 309 return; 310 } 311 312 case eRejectedPartitionedRequiresSecure: { 313 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 314 "CookieRejectedPartitionedRequiresSecure"_ns); 315 return; 316 } 317 318 case eRejectedNameValueOversize: { 319 *aFlags = nsIScriptError::warningFlag; 320 aCategory.Assign(CONSOLE_OVERSIZE_CATEGORY); 321 aKey.Assign("CookieOversize"_ns); 322 323 aParams.AppendElement(NS_ConvertUTF8toUTF16(mCookieData.name())); 324 325 nsString size; 326 size.AppendInt(kMaxBytesPerCookie); 327 aParams.AppendElement(size); 328 return; 329 } 330 331 case eRejectedInvalidCharName: { 332 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 333 "CookieRejectedInvalidCharName"_ns); 334 return; 335 } 336 337 case eRejectedInvalidCharValue: { 338 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 339 "CookieRejectedInvalidCharValue"_ns); 340 return; 341 } 342 343 case eRejectedAttributePathOversize: { 344 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 345 "CookieRejectedAttributePathOversize"_ns); 346 return; 347 } 348 349 case eRejectedAttributeDomainOversize: { 350 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 351 "CookieRejectedAttributeDomainOversize"_ns); 352 return; 353 } 354 355 case eRejectedAttributeExpiryOversize: { 356 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 357 "CookieRejectedAttributeExpiryOversize"_ns); 358 return; 359 } 360 361 case eRejectedInvalidPath: { 362 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, "CookieRejectedInvalidPath"_ns); 363 return; 364 } 365 366 case eRejectedInvalidDomain: { 367 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 368 "CookieRejectedInvalidDomain"_ns); 369 return; 370 } 371 372 case eRejectedInvalidPrefix: { 373 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 374 "CookieRejectedInvalidPrefix"_ns); 375 return; 376 } 377 378 case eRejectedHttpOnlyButFromScript: { 379 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 380 "CookieRejectedHttpOnlyButFromScript"_ns); 381 return; 382 } 383 384 case eRejectedSecureButNonHttps: { 385 SET_LOG_DATA(CONSOLE_REJECTION_CATEGORY, 386 "CookieRejectedSecureButNonHttps"_ns); 387 return; 388 } 389 390 case eRejectedForNonSameSiteness: { 391 SET_LOG_DATA(CONSOLE_SAMESITE_CATEGORY, 392 "CookieRejectedForNonSameSiteness"_ns); 393 return; 394 } 395 } 396 397 #undef SET_LOG_DATA 398 } 399 400 void CookieValidation::ReportErrorsAndWarnings(nsIConsoleReportCollector* aCRC, 401 nsIURI* aHostURI) const { 402 if (mResult != eOK) { 403 uint32_t flags; 404 nsAutoCString category; 405 nsAutoCString key; 406 nsTArray<nsString> params; 407 408 RetrieveErrorLogData(&flags, category, key, params); 409 410 CookieLogging::LogMessageToConsole(aCRC, aHostURI, flags, category, key, 411 params); 412 return; 413 } 414 415 if (mWarnings.mSameSiteNoneRequiresSecureForBeta) { 416 CookieLogging::LogMessageToConsole( 417 aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, 418 "CookieRejectedNonRequiresSecureForBeta3"_ns, 419 AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(mCookieData.name()), 420 SAMESITE_MDN_URL}); 421 } 422 423 if (mWarnings.mSameSiteLaxForced) { 424 CookieLogging::LogMessageToConsole( 425 aCRC, aHostURI, nsIScriptError::infoFlag, CONSOLE_SAMESITE_CATEGORY, 426 "CookieLaxForced2"_ns, 427 AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(mCookieData.name())}); 428 } 429 430 if (mWarnings.mSameSiteLaxForcedForBeta) { 431 CookieLogging::LogMessageToConsole( 432 aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_SAMESITE_CATEGORY, 433 "CookieLaxForcedForBeta2"_ns, 434 AutoTArray<nsString, 2>{NS_ConvertUTF8toUTF16(mCookieData.name()), 435 SAMESITE_MDN_URL}); 436 } 437 } 438 439 NS_IMETHODIMP 440 CookieValidation::GetErrorString(nsAString& aResult) { 441 if (mResult == eOK) { 442 return NS_OK; 443 } 444 445 uint32_t flags; 446 nsAutoCString category; 447 nsAutoCString key; 448 nsTArray<nsString> params; 449 450 RetrieveErrorLogData(&flags, category, key, params); 451 452 return nsContentUtils::FormatLocalizedString( 453 nsContentUtils::eNECKO_PROPERTIES_en_US, key.get(), params, aResult); 454 } 455 456 // static 457 bool CookieValidation::CheckNameAndValueSize(const CookieStruct& aCookieData) { 458 // reject cookie if it's over the size limit, per RFC2109 459 return (aCookieData.name().Length() + aCookieData.value().Length()) <= 460 kMaxBytesPerCookie; 461 } 462 463 bool CookieValidation::CheckName(const CookieStruct& aCookieData) { 464 if (!aCookieData.name().IsEmpty() && (aCookieData.name().First() == 0x20 || 465 aCookieData.name().Last() == 0x20)) { 466 return false; 467 } 468 469 const char illegalNameCharacters[] = { 470 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 0x0D, 471 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 472 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x3D, 0x7F, 0x00}; 473 474 const auto* start = aCookieData.name().BeginReading(); 475 const auto* end = aCookieData.name().EndReading(); 476 477 auto charFilter = [&](unsigned char c) { 478 if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) { 479 return true; 480 } 481 return std::find(std::begin(illegalNameCharacters), 482 std::end(illegalNameCharacters), 483 c) != std::end(illegalNameCharacters); 484 }; 485 486 return std::find_if(start, end, charFilter) == end; 487 } 488 489 bool CookieValidation::CheckValue(const CookieStruct& aCookieData) { 490 if (!aCookieData.value().IsEmpty() && (aCookieData.value().First() == 0x20 || 491 aCookieData.value().Last() == 0x20)) { 492 return false; 493 } 494 495 // reject cookie if value contains an RFC 6265 disallowed character - see 496 // https://bugzilla.mozilla.org/show_bug.cgi?id=1191423 497 // NOTE: this is not the full set of characters disallowed by 6265 - notably 498 // 0x09, 0x20, 0x22, 0x2C, and 0x5C are missing from this list. 499 const char illegalCharacters[] = { 500 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x0A, 0x0B, 0x0C, 501 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 502 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x3B, 0x7F, 0x00}; 503 504 const auto* start = aCookieData.value().BeginReading(); 505 const auto* end = aCookieData.value().EndReading(); 506 507 bool shouldBlockEqualInNamelessCookie = 508 aCookieData.name().IsEmpty() && 509 StaticPrefs::network_cookie_block_nameless_with_equal_char(); 510 511 auto charFilter = [&](unsigned char c) { 512 if (StaticPrefs::network_cookie_blockUnicode() && c >= 0x80) { 513 return true; 514 } 515 516 if (c == '=' && shouldBlockEqualInNamelessCookie) { 517 return true; 518 } 519 520 return std::find(std::begin(illegalCharacters), std::end(illegalCharacters), 521 c) != std::end(illegalCharacters); 522 }; 523 524 return std::find_if(start, end, charFilter) == end; 525 }