OriginAttributes.cpp (17036B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=2 sw=2 et tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/OriginAttributes.h" 8 #include "mozilla/Assertions.h" 9 #include "mozilla/Preferences.h" 10 #include "mozilla/dom/BlobURLProtocolHandler.h" 11 #include "mozilla/dom/quota/QuotaManager.h" 12 #include "nsIEffectiveTLDService.h" 13 #include "nsIURI.h" 14 #include "nsNetCID.h" 15 #include "nsNetUtil.h" 16 #include "nsString.h" 17 #include "nsURLHelper.h" 18 19 static const char kSourceChar = ':'; 20 static const char kSanitizedChar = '+'; 21 22 namespace mozilla { 23 24 static void MakeTopLevelInfo(const nsACString& aScheme, const nsACString& aHost, 25 bool aForeignByAncestorContext, bool aUseSite, 26 nsAString& aTopLevelInfo) { 27 if (!aUseSite) { 28 aTopLevelInfo.Assign(NS_ConvertUTF8toUTF16(aHost)); 29 return; 30 } 31 32 // Note: If you change the serialization of the partition-key, please update 33 // StoragePrincipalHelper.cpp too. 34 35 nsAutoCString site; 36 site.AssignLiteral("("); 37 site.Append(aScheme); 38 site.Append(","); 39 site.Append(aHost); 40 if (aForeignByAncestorContext) { 41 site.Append(",f"); 42 } 43 site.AppendLiteral(")"); 44 45 aTopLevelInfo.Assign(NS_ConvertUTF8toUTF16(site)); 46 } 47 48 static void PopulateTopLevelInfoFromURI(const bool aIsTopLevelDocument, 49 nsIURI* aURI, 50 bool aForeignByAncestorContext, 51 bool aIsFirstPartyEnabled, bool aForced, 52 bool aUseSite, 53 nsString OriginAttributes::* aTarget, 54 OriginAttributes& aOriginAttributes) { 55 nsresult rv; 56 57 if (!aURI) { 58 return; 59 } 60 61 // If the prefs are off or this is not a top level load, bail out. 62 if ((!aIsFirstPartyEnabled || !aIsTopLevelDocument) && !aForced) { 63 return; 64 } 65 66 nsAString& topLevelInfo = aOriginAttributes.*aTarget; 67 68 nsAutoCString scheme; 69 nsCOMPtr<nsIURI> uri = aURI; 70 // The URI could be nested (for example view-source:http://example.com), in 71 // that case we want to get the innermost URI (http://example.com). 72 nsCOMPtr<nsINestedURI> nestedURI; 73 do { 74 NS_ENSURE_SUCCESS_VOID(uri->GetScheme(scheme)); 75 nestedURI = do_QueryInterface(uri); 76 // We can't just use GetInnermostURI on the nested URI, since that would 77 // also unwrap some about: URIs to hidden moz-safe-about: URIs, which we do 78 // not want. Thus we loop through with GetInnerURI until the URI isn't 79 // nested anymore or we encounter a about: scheme. 80 } while (nestedURI && !scheme.EqualsLiteral("about") && 81 NS_SUCCEEDED(nestedURI->GetInnerURI(getter_AddRefs(uri)))); 82 83 if (scheme.EqualsLiteral("about")) { 84 MakeTopLevelInfo(scheme, nsLiteralCString(ABOUT_URI_FIRST_PARTY_DOMAIN), 85 aForeignByAncestorContext, aUseSite, topLevelInfo); 86 return; 87 } 88 89 // If a null principal URI was provided, extract the UUID portion of the URI 90 // to use for the first-party domain. 91 if (scheme.EqualsLiteral("moz-nullprincipal")) { 92 // Get the UUID portion of the URI, ignoring the precursor principal. 93 nsAutoCString filePath; 94 rv = uri->GetFilePath(filePath); 95 MOZ_ASSERT(NS_SUCCEEDED(rv)); 96 // Remove the `{}` characters from both ends. 97 filePath.Mid(filePath, 1, filePath.Length() - 2); 98 filePath.AppendLiteral(".mozilla"); 99 // Store the generated file path. 100 topLevelInfo = NS_ConvertUTF8toUTF16(filePath); 101 return; 102 } 103 104 // Add-on principals should never get any first-party domain 105 // attributes in order to guarantee their storage integrity when switching 106 // FPI on and off. 107 if (scheme.EqualsLiteral("moz-extension")) { 108 return; 109 } 110 111 nsCOMPtr<nsIPrincipal> blobPrincipal; 112 if (dom::BlobURLProtocolHandler::GetBlobURLPrincipal( 113 uri, getter_AddRefs(blobPrincipal))) { 114 MOZ_ASSERT(blobPrincipal); 115 topLevelInfo = blobPrincipal->OriginAttributesRef().*aTarget; 116 return; 117 } 118 119 nsCOMPtr<nsIEffectiveTLDService> tldService = 120 do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID); 121 MOZ_ASSERT(tldService); 122 NS_ENSURE_TRUE_VOID(tldService); 123 124 nsAutoCString baseDomain; 125 rv = tldService->GetBaseDomain(uri, 0, baseDomain); 126 if (NS_SUCCEEDED(rv)) { 127 MakeTopLevelInfo(scheme, baseDomain, aForeignByAncestorContext, aUseSite, 128 topLevelInfo); 129 return; 130 } 131 132 // Saving before rv is overwritten. 133 bool isIpAddress = (rv == NS_ERROR_HOST_IS_IP_ADDRESS); 134 bool isInsufficientDomainLevels = (rv == NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS); 135 136 nsAutoCString host; 137 rv = uri->GetHost(host); 138 NS_ENSURE_SUCCESS_VOID(rv); 139 140 if (isIpAddress) { 141 // If the host is an IPv4/IPv6 address, we still accept it as a 142 // valid topLevelInfo. 143 nsAutoCString ipAddr; 144 145 if (net_IsValidIPv6Addr(host)) { 146 // According to RFC2732, the host of an IPv6 address should be an 147 // IPv6reference. The GetHost() of nsIURI will only return the IPv6 148 // address. So, we need to convert it back to IPv6reference here. 149 ipAddr.AssignLiteral("["); 150 ipAddr.Append(host); 151 ipAddr.AppendLiteral("]"); 152 } else { 153 ipAddr = host; 154 } 155 MakeTopLevelInfo(scheme, ipAddr, aForeignByAncestorContext, aUseSite, 156 topLevelInfo); 157 return; 158 } 159 160 if (aUseSite) { 161 MakeTopLevelInfo(scheme, host, aForeignByAncestorContext, aUseSite, 162 topLevelInfo); 163 return; 164 } 165 166 if (isInsufficientDomainLevels) { 167 nsAutoCString publicSuffix; 168 rv = tldService->GetPublicSuffix(uri, publicSuffix); 169 if (NS_SUCCEEDED(rv)) { 170 MakeTopLevelInfo(scheme, publicSuffix, aForeignByAncestorContext, 171 aUseSite, topLevelInfo); 172 return; 173 } 174 } 175 } 176 177 void OriginAttributes::SetFirstPartyDomain(const bool aIsTopLevelDocument, 178 nsIURI* aURI, bool aForced) { 179 PopulateTopLevelInfoFromURI( 180 aIsTopLevelDocument, aURI, false, IsFirstPartyEnabled(), aForced, 181 StaticPrefs::privacy_firstparty_isolate_use_site(), 182 &OriginAttributes::mFirstPartyDomain, *this); 183 } 184 185 void OriginAttributes::SetFirstPartyDomain(const bool aIsTopLevelDocument, 186 const nsACString& aDomain) { 187 SetFirstPartyDomain(aIsTopLevelDocument, NS_ConvertUTF8toUTF16(aDomain)); 188 } 189 190 void OriginAttributes::SetFirstPartyDomain(const bool aIsTopLevelDocument, 191 const nsAString& aDomain, 192 bool aForced) { 193 // If the pref is off or this is not a top level load, bail out. 194 if ((!IsFirstPartyEnabled() || !aIsTopLevelDocument) && !aForced) { 195 return; 196 } 197 198 mFirstPartyDomain = aDomain; 199 } 200 201 void OriginAttributes::SetPartitionKey(nsIURI* aURI, 202 bool aForeignByAncestorContext) { 203 PopulateTopLevelInfoFromURI( 204 false /* aIsTopLevelDocument */, aURI, aForeignByAncestorContext, 205 IsFirstPartyEnabled(), true /* aForced */, 206 StaticPrefs::privacy_dynamic_firstparty_use_site(), 207 &OriginAttributes::mPartitionKey, *this); 208 } 209 210 void OriginAttributes::SetPartitionKey(const nsACString& aOther) { 211 SetPartitionKey(NS_ConvertUTF8toUTF16(aOther)); 212 } 213 214 void OriginAttributes::SetPartitionKey(const nsAString& aOther) { 215 mPartitionKey = aOther; 216 } 217 218 void OriginAttributes::CreateSuffix(nsACString& aStr) const { 219 URLParams params; 220 nsAutoCString value; 221 222 // 223 // Important: While serializing any string-valued attributes, perform a 224 // release-mode assertion to make sure that they don't contain characters that 225 // will break the quota manager when it uses the serialization for file 226 // naming. 227 // 228 229 if (mUserContextId != nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID) { 230 value.Truncate(); 231 value.AppendInt(mUserContextId); 232 params.Set("userContextId"_ns, value); 233 } 234 235 if (mPrivateBrowsingId) { 236 value.Truncate(); 237 value.AppendInt(mPrivateBrowsingId); 238 params.Set("privateBrowsingId"_ns, value); 239 } 240 241 if (!mFirstPartyDomain.IsEmpty()) { 242 nsAutoString sanitizedFirstPartyDomain(mFirstPartyDomain); 243 sanitizedFirstPartyDomain.ReplaceChar(kSourceChar, kSanitizedChar); 244 params.Set("firstPartyDomain"_ns, 245 NS_ConvertUTF16toUTF8(sanitizedFirstPartyDomain)); 246 } 247 248 if (!mGeckoViewSessionContextId.IsEmpty()) { 249 nsAutoString sanitizedGeckoViewUserContextId(mGeckoViewSessionContextId); 250 sanitizedGeckoViewUserContextId.ReplaceChar( 251 dom::quota::QuotaManager::kReplaceChars16, kSanitizedChar); 252 params.Set("geckoViewUserContextId"_ns, 253 NS_ConvertUTF16toUTF8(sanitizedGeckoViewUserContextId)); 254 } 255 256 if (!mPartitionKey.IsEmpty()) { 257 nsAutoString sanitizedPartitionKey(mPartitionKey); 258 sanitizedPartitionKey.ReplaceChar(kSourceChar, kSanitizedChar); 259 params.Set("partitionKey"_ns, NS_ConvertUTF16toUTF8(sanitizedPartitionKey)); 260 } 261 262 aStr.Truncate(); 263 264 params.Serialize(value, true); 265 if (!value.IsEmpty()) { 266 aStr.AppendLiteral("^"); 267 aStr.Append(value); 268 } 269 270 // In debug builds, check the whole string for illegal characters too (just in 271 // case). 272 #ifdef DEBUG 273 nsAutoCString str; 274 str.Assign(aStr); 275 MOZ_ASSERT(str.FindCharInSet(dom::quota::QuotaManager::kReplaceChars) == 276 kNotFound); 277 #endif 278 } 279 280 already_AddRefed<nsAtom> OriginAttributes::CreateSuffixAtom() const { 281 nsAutoCString suffix; 282 CreateSuffix(suffix); 283 return NS_Atomize(suffix); 284 } 285 286 void OriginAttributes::CreateAnonymizedSuffix(nsACString& aStr) const { 287 OriginAttributes attrs = *this; 288 289 if (!attrs.mFirstPartyDomain.IsEmpty()) { 290 attrs.mFirstPartyDomain.AssignLiteral("_anonymizedFirstPartyDomain_"); 291 } 292 293 if (!attrs.mPartitionKey.IsEmpty()) { 294 attrs.mPartitionKey.AssignLiteral("_anonymizedPartitionKey_"); 295 } 296 297 attrs.CreateSuffix(aStr); 298 } 299 300 bool OriginAttributes::PopulateFromSuffix(const nsACString& aStr) { 301 if (aStr.IsEmpty()) { 302 return true; 303 } 304 305 if (aStr[0] != '^') { 306 return false; 307 } 308 309 // If a non-default mPrivateBrowsingId is passed and is not present in the 310 // suffix, then it will retain the id when it should be default according 311 // to the suffix. Set to default before iterating to fix this. 312 mPrivateBrowsingId = nsIScriptSecurityManager::DEFAULT_PRIVATE_BROWSING_ID; 313 314 // Checking that we are in a pristine state 315 316 MOZ_RELEASE_ASSERT(mUserContextId == 0); 317 MOZ_RELEASE_ASSERT(mPrivateBrowsingId == 0); 318 MOZ_RELEASE_ASSERT(mFirstPartyDomain.IsEmpty()); 319 MOZ_RELEASE_ASSERT(mGeckoViewSessionContextId.IsEmpty()); 320 MOZ_RELEASE_ASSERT(mPartitionKey.IsEmpty()); 321 322 return URLParams::Parse( 323 Substring(aStr, 1, aStr.Length() - 1), true, 324 [this](const nsACString& aName, const nsACString& aValue) { 325 if (aName.EqualsLiteral("inBrowser")) { 326 if (!aValue.EqualsLiteral("1")) { 327 return false; 328 } 329 330 return true; 331 } 332 333 if (aName.EqualsLiteral("addonId") || aName.EqualsLiteral("appId")) { 334 // No longer supported. Silently ignore so that legacy origin strings 335 // don't cause failures. 336 return true; 337 } 338 339 if (aName.EqualsLiteral("userContextId")) { 340 nsresult rv; 341 int64_t val = aValue.ToInteger64(&rv); 342 NS_ENSURE_SUCCESS(rv, false); 343 NS_ENSURE_TRUE(val <= UINT32_MAX, false); 344 mUserContextId = static_cast<uint32_t>(val); 345 346 return true; 347 } 348 349 if (aName.EqualsLiteral("privateBrowsingId")) { 350 nsresult rv; 351 int64_t val = aValue.ToInteger64(&rv); 352 NS_ENSURE_SUCCESS(rv, false); 353 NS_ENSURE_TRUE(val >= 0 && val <= UINT32_MAX, false); 354 mPrivateBrowsingId = static_cast<uint32_t>(val); 355 356 return true; 357 } 358 359 if (aName.EqualsLiteral("firstPartyDomain")) { 360 nsAutoCString firstPartyDomain(aValue); 361 firstPartyDomain.ReplaceChar(kSanitizedChar, kSourceChar); 362 mFirstPartyDomain.Assign(NS_ConvertUTF8toUTF16(firstPartyDomain)); 363 return true; 364 } 365 366 if (aName.EqualsLiteral("geckoViewUserContextId")) { 367 mGeckoViewSessionContextId.Assign(NS_ConvertUTF8toUTF16(aValue)); 368 return true; 369 } 370 371 if (aName.EqualsLiteral("partitionKey")) { 372 nsAutoCString partitionKey(aValue); 373 partitionKey.ReplaceChar(kSanitizedChar, kSourceChar); 374 mPartitionKey.Assign(NS_ConvertUTF8toUTF16(partitionKey)); 375 return true; 376 } 377 378 // No other attributes are supported. 379 return false; 380 }); 381 } 382 383 bool OriginAttributes::PopulateFromOrigin(const nsACString& aOrigin, 384 nsACString& aOriginNoSuffix) { 385 // RFindChar is only available on nsCString. 386 nsCString origin(aOrigin); 387 int32_t pos = origin.RFindChar('^'); 388 389 if (pos == kNotFound) { 390 aOriginNoSuffix = origin; 391 return true; 392 } 393 394 aOriginNoSuffix = Substring(origin, 0, pos); 395 return PopulateFromSuffix(Substring(origin, pos)); 396 } 397 398 void OriginAttributes::SyncAttributesWithPrivateBrowsing( 399 bool aInPrivateBrowsing) { 400 mPrivateBrowsingId = aInPrivateBrowsing ? 1 : 0; 401 } 402 403 /* static */ 404 bool OriginAttributes::IsPrivateBrowsing(const nsACString& aOrigin) { 405 nsAutoCString dummy; 406 OriginAttributes attrs; 407 if (NS_WARN_IF(!attrs.PopulateFromOrigin(aOrigin, dummy))) { 408 return false; 409 } 410 411 return attrs.IsPrivateBrowsing(); 412 } 413 414 /* static */ 415 bool OriginAttributes::ParsePartitionKey(const nsAString& aPartitionKey, 416 nsAString& outScheme, 417 nsAString& outBaseDomain, 418 int32_t& outPort, 419 bool& outForeignByAncestorContext) { 420 outScheme.Truncate(); 421 outBaseDomain.Truncate(); 422 outPort = -1; 423 outForeignByAncestorContext = false; 424 425 // Partition keys have the format 426 // "(<scheme>,<baseDomain>[,port][,foreignancestorbit])". The port and 427 // ancestor bits are optional. For example: "(https,example.com,8443)" or 428 // "(http,example.org)", or "(http,example.info,f)", or 429 // "(http,example.biz,8443,f)". When privacy.dynamic_firstparty.use_site = 430 // false, the partitionKey contains only the host, e.g. "example.com". See 431 // MakeTopLevelInfo for the partitionKey serialization code. 432 433 if (aPartitionKey.IsEmpty()) { 434 return true; 435 } 436 437 // PartitionKey contains only the host. 438 if (!StaticPrefs::privacy_dynamic_firstparty_use_site()) { 439 outBaseDomain = aPartitionKey; 440 return true; 441 } 442 443 // Smallest possible partitionKey is "(x,x)". Scheme and base domain are 444 // mandatory. 445 if (NS_WARN_IF(aPartitionKey.Length() < 5)) { 446 return false; 447 } 448 449 if (NS_WARN_IF(aPartitionKey.First() != '(' || aPartitionKey.Last() != ')')) { 450 return false; 451 } 452 453 // Remove outer brackets so we can string split. 454 nsAutoString str(Substring(aPartitionKey, 1, aPartitionKey.Length() - 2)); 455 456 uint32_t fieldIndex = 0; 457 for (const nsAString& field : str.Split(',')) { 458 if (NS_WARN_IF(field.IsEmpty())) { 459 // There cannot be empty fields. 460 return false; 461 } 462 463 if (fieldIndex == 0) { 464 outScheme.Assign(field); 465 } else if (fieldIndex == 1) { 466 outBaseDomain.Assign(field); 467 } else if (fieldIndex == 2) { 468 // The first optional argument is either "f" or a port number 469 if (field.EqualsLiteral("f")) { 470 outForeignByAncestorContext = true; 471 } else { 472 // Parse the port which is represented in the partitionKey string as a 473 // decimal (base 10) number. 474 long port = strtol(NS_ConvertUTF16toUTF8(field).get(), nullptr, 10); 475 // Invalid port. 476 if (NS_WARN_IF(port == 0)) { 477 return false; 478 } 479 outPort = static_cast<int32_t>(port); 480 } 481 } else if (fieldIndex == 3) { 482 // The second optional argument, if it exists, is "f" and the first 483 // optional argument was a port 484 if (!field.EqualsLiteral("f") || outPort != -1) { 485 NS_WARNING("Invalid partitionKey. Invalid token."); 486 return false; 487 } 488 outForeignByAncestorContext = true; 489 } else { 490 NS_WARNING("Invalid partitionKey. Too many tokens"); 491 return false; 492 } 493 494 fieldIndex++; 495 } 496 497 // scheme and base domain are required. 498 return fieldIndex > 1; 499 } 500 501 /* static */ 502 bool OriginAttributes::ExtractSiteFromPartitionKey( 503 const nsAString& aPartitionKey, nsAString& aOutSite) { 504 nsAutoString scheme, host; 505 int32_t port; 506 bool unused; 507 if (!ParsePartitionKey(aPartitionKey, scheme, host, port, unused)) { 508 return false; 509 } 510 511 if (port == -1) { 512 aOutSite.Assign(scheme + u"://"_ns + host); 513 } else { 514 aOutSite.Assign(scheme + u"://"_ns + host + u":"_ns); 515 aOutSite.AppendInt(port); 516 } 517 return true; 518 } 519 520 } // namespace mozilla