summaryrefslogtreecommitdiffstats
path: root/netwerk/cookie
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/cookie')
-rw-r--r--netwerk/cookie/CookieCommons.cpp42
-rw-r--r--netwerk/cookie/CookieService.cpp207
-rw-r--r--netwerk/cookie/CookieService.h1
-rw-r--r--netwerk/cookie/CookieServiceChild.cpp140
-rw-r--r--netwerk/cookie/CookieServiceParent.cpp35
-rw-r--r--netwerk/cookie/test/browser/browser.toml6
-rw-r--r--netwerk/cookie/test/browser/browser_cookie_chips.js539
-rw-r--r--netwerk/cookie/test/browser/browser_cookies_serviceWorker.js540
-rw-r--r--netwerk/cookie/test/browser/chips.sjs28
-rw-r--r--netwerk/cookie/test/browser/cookies.sjs17
-rw-r--r--netwerk/cookie/test/browser/serviceWorker.js21
11 files changed, 1459 insertions, 117 deletions
diff --git a/netwerk/cookie/CookieCommons.cpp b/netwerk/cookie/CookieCommons.cpp
index 9b26fc4a6e..4c4ae0c848 100644
--- a/netwerk/cookie/CookieCommons.cpp
+++ b/netwerk/cookie/CookieCommons.cpp
@@ -349,10 +349,6 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument(
std::function<bool(const nsACString&, const OriginAttributes&)>&&
aHasExistingCookiesLambda,
nsIURI** aDocumentURI, nsACString& aBaseDomain, OriginAttributes& aAttrs) {
- nsCOMPtr<nsIPrincipal> storagePrincipal =
- aDocument->EffectiveCookiePrincipal();
- MOZ_ASSERT(storagePrincipal);
-
nsCOMPtr<nsIURI> principalURI;
auto* basePrincipal = BasePrincipal::Cast(aDocument->NodePrincipal());
basePrincipal->GetURI(getter_AddRefs(principalURI));
@@ -379,15 +375,6 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument(
return nullptr;
}
- // Check if limit-foreign is required.
- uint32_t dummyRejectedReason = 0;
- if (aDocument->CookieJarSettings()->GetLimitForeignContexts() &&
- !aHasExistingCookiesLambda(baseDomain,
- storagePrincipal->OriginAttributesRef()) &&
- !ShouldAllowAccessFor(innerWindow, principalURI, &dummyRejectedReason)) {
- return nullptr;
- }
-
bool isForeignAndNotAddon = false;
if (!BasePrincipal::Cast(aDocument->NodePrincipal())->AddonPolicy()) {
rv = aThirdPartyUtil->IsThirdPartyWindow(
@@ -439,8 +426,29 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument(
return nullptr;
}
+ // CHIPS - If the partitioned attribute is set, store cookie in partitioned
+ // cookie jar independent of context. If the cookies are stored in the
+ // partitioned cookie jar anyway no special treatment of CHIPS cookies
+ // necessary.
+ bool needPartitioned =
+ StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() &&
+ cookieData.isPartitioned();
+ nsCOMPtr<nsIPrincipal> cookiePrincipal =
+ needPartitioned ? aDocument->PartitionedPrincipal()
+ : aDocument->EffectiveCookiePrincipal();
+ MOZ_ASSERT(cookiePrincipal);
+
+ // Check if limit-foreign is required.
+ uint32_t dummyRejectedReason = 0;
+ if (aDocument->CookieJarSettings()->GetLimitForeignContexts() &&
+ !aHasExistingCookiesLambda(baseDomain,
+ cookiePrincipal->OriginAttributesRef()) &&
+ !ShouldAllowAccessFor(innerWindow, principalURI, &dummyRejectedReason)) {
+ return nullptr;
+ }
+
RefPtr<Cookie> cookie =
- Cookie::Create(cookieData, storagePrincipal->OriginAttributesRef());
+ Cookie::Create(cookieData, cookiePrincipal->OriginAttributesRef());
MOZ_ASSERT(cookie);
cookie->SetLastAccessed(currentTimeInUsec);
@@ -448,7 +456,7 @@ already_AddRefed<Cookie> CookieCommons::CreateCookieFromDocument(
Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
aBaseDomain = baseDomain;
- aAttrs = storagePrincipal->OriginAttributesRef();
+ aAttrs = cookiePrincipal->OriginAttributesRef();
principalURI.forget(aDocumentURI);
return cookie.forget();
@@ -486,9 +494,11 @@ bool CookieCommons::ShouldIncludeCrossSiteCookieForDocument(
int32_t sameSiteAttr = 0;
aCookie->GetSameSite(&sameSiteAttr);
+ // CHIPS - If a third-party has storage access it can access both it's
+ // partitioned and unpartitioned cookie jars, else its cookies are blocked.
if (aDocument->CookieJarSettings()->GetPartitionForeign() &&
StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() &&
- !aCookie->IsPartitioned()) {
+ !aCookie->IsPartitioned() && !aDocument->UsingStorageAccess()) {
return false;
}
diff --git a/netwerk/cookie/CookieService.cpp b/netwerk/cookie/CookieService.cpp
index 0e90b8cbae..78698d44a0 100644
--- a/netwerk/cookie/CookieService.cpp
+++ b/netwerk/cookie/CookieService.cpp
@@ -373,15 +373,6 @@ CookieService::GetCookieStringFromDocument(Document* aDocument,
return NS_OK;
}
- nsCOMPtr<nsIPrincipal> cookiePrincipal =
- aDocument->EffectiveCookiePrincipal();
-
- // TODO (Bug 1874174): A document could access both unpartitioned and
- // partitioned cookie jars. We will need to prepare partitioned and
- // unpartitioned principals for access both cookie jars.
- nsTArray<nsCOMPtr<nsIPrincipal>> principals;
- principals.AppendElement(cookiePrincipal);
-
bool thirdParty = true;
nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
// in gtests we don't have a window, let's consider those requests as 3rd
@@ -395,6 +386,26 @@ CookieService::GetCookieStringFromDocument(Document* aDocument,
}
}
+ nsCOMPtr<nsIPrincipal> cookiePrincipal =
+ aDocument->EffectiveCookiePrincipal();
+
+ nsTArray<nsCOMPtr<nsIPrincipal>> principals;
+ principals.AppendElement(cookiePrincipal);
+
+ // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available
+ // (and therefore the partitioned principal), the unpartitioned cookie jar is
+ // only available in first-party or third-party with storageAccess contexts.
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ bool isUnpartitioned =
+ cookiePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty();
+ if (isCHIPS && isUnpartitioned) {
+ // Assert that we are only doing this if we are first-party or third-party
+ // with storageAccess.
+ MOZ_ASSERT(!thirdParty || aDocument->UsingStorageAccess());
+ // Add the partitioned principal to principals
+ principals.AppendElement(aDocument->PartitionedPrincipal());
+ }
+
nsTArray<Cookie*> cookieList;
for (auto& principal : principals) {
@@ -519,20 +530,40 @@ CookieService::GetCookieStringFromHttp(nsIURI* aHostURI, nsIChannel* aChannel,
ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel(
aChannel, false, aHostURI, nullptr, &rejectedReason);
- OriginAttributes attrs;
- StoragePrincipalHelper::GetOriginAttributes(
- aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal);
-
bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel);
bool hadCrossSiteRedirects = false;
bool isSameSiteForeign = CookieCommons::IsSameSiteForeign(
aChannel, aHostURI, &hadCrossSiteRedirects);
- // TODO (Bug 1874174): A channel could load both unpartitioned and partitioned
- // cookie jars together. We will need to get cookies from both unpartitioned
- // and partitioned cookie jars according to storage access.
+ OriginAttributes storageOriginAttributes;
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, storageOriginAttributes,
+ StoragePrincipalHelper::eStorageAccessPrincipal);
+
nsTArray<OriginAttributes> originAttributesList;
- originAttributesList.AppendElement(attrs);
+ originAttributesList.AppendElement(storageOriginAttributes);
+
+ // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available
+ // (and therefore the partitioned OriginAttributes), the unpartitioned cookie
+ // jar is only available in first-party or third-party with storageAccess
+ // contexts.
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ bool isUnpartitioned = storageOriginAttributes.mPartitionKey.IsEmpty();
+ if (isCHIPS && isUnpartitioned) {
+ // Assert that we are only doing this if we are first-party or third-party
+ // with storageAccess.
+ MOZ_ASSERT(
+ !result.contains(ThirdPartyAnalysis::IsForeign) ||
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted));
+ // Add the partitioned principal to principals
+ OriginAttributes partitionedOriginAttributes;
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, partitionedOriginAttributes,
+ StoragePrincipalHelper::ePartitionedPrincipal);
+ originAttributesList.AppendElement(partitionedOriginAttributes);
+ // Assert partitionedOAs have partitioneKey set.
+ MOZ_ASSERT(!partitionedOriginAttributes.mPartitionKey.IsEmpty());
+ }
AutoTArray<Cookie*, 8> foundCookieList;
GetCookiesForURI(
@@ -630,9 +661,10 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
ThirdPartyAnalysisResult result = mThirdPartyUtil->AnalyzeChannel(
aChannel, false, aHostURI, nullptr, &rejectedReason);
- OriginAttributes attrs;
+ OriginAttributes storagePrincipalOriginAttributes;
StoragePrincipalHelper::GetOriginAttributes(
- aChannel, attrs, StoragePrincipalHelper::eStorageAccessPrincipal);
+ aChannel, storagePrincipalOriginAttributes,
+ StoragePrincipalHelper::eStorageAccessPrincipal);
// get the base domain for the host URI.
// e.g. for "www.bbc.co.uk", this would be "bbc.co.uk".
@@ -660,11 +692,11 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
baseDomainFromURI);
NS_ENSURE_SUCCESS(rv, NS_OK);
- CookieStorage* storage = PickStorage(attrs);
+ CookieStorage* storage = PickStorage(storagePrincipalOriginAttributes);
// check default prefs
uint32_t priorCookieCount = storage->CountCookiesFromHost(
- baseDomainFromURI, attrs.mPrivateBrowsingId);
+ baseDomainFromURI, storagePrincipalOriginAttributes.mPrivateBrowsingId);
nsCOMPtr<nsIConsoleReportCollector> crc = do_QueryInterface(aChannel);
@@ -674,7 +706,8 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
- aCookieHeader, priorCookieCount, attrs, &rejectedReason);
+ aCookieHeader, priorCookieCount, storagePrincipalOriginAttributes,
+ &rejectedReason);
MOZ_ASSERT_IF(rejectedReason, cookieStatus == STATUS_REJECTED);
@@ -718,9 +751,24 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
nsCString cookieHeader(aCookieHeader);
- bool moreCookieToRead = true;
+ // CHIPS - The partitioned cookie jar is always available and it is always
+ // possible to store cookies in it using the "Partitioned" attribute.
+ // Prepare the partitioned principals OAs to enable possible partitioned
+ // cookie storing from first-party or with StorageAccess.
+ // Similar behavior to CookieServiceChild::SetCookieStringFromHttp().
+ OriginAttributes partitionedPrincipalOriginAttributes;
+ bool isPartitionedPrincipal =
+ !storagePrincipalOriginAttributes.mPartitionKey.IsEmpty();
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ // Only need to get OAs if we don't already use the partitioned principal.
+ if (isCHIPS && !isPartitionedPrincipal) {
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, partitionedPrincipalOriginAttributes,
+ StoragePrincipalHelper::ePartitionedPrincipal);
+ }
// process each cookie in the header
+ bool moreCookieToRead = true;
while (moreCookieToRead) {
CookieStruct cookieData;
bool canSetCookie = false;
@@ -751,8 +799,22 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
continue;
}
+ // CHIPS - If the partitioned attribute is set, store cookie in partitioned
+ // cookie jar independent of context. If the cookies are stored in the
+ // partitioned cookie jar anyway no special treatment of CHIPS cookies
+ // necessary.
+ bool needPartitioned =
+ isCHIPS && cookieData.isPartitioned() && !isPartitionedPrincipal;
+ OriginAttributes& cookieOriginAttributes =
+ needPartitioned ? partitionedPrincipalOriginAttributes
+ : storagePrincipalOriginAttributes;
+ // Assert that partitionedPrincipalOriginAttributes are initialized if used.
+ MOZ_ASSERT_IF(
+ needPartitioned,
+ !partitionedPrincipalOriginAttributes.mPartitionKey.IsEmpty());
+
// create a new Cookie
- RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs);
+ RefPtr<Cookie> cookie = Cookie::Create(cookieData, cookieOriginAttributes);
MOZ_ASSERT(cookie);
int64_t currentTimeInUsec = PR_Now();
@@ -763,8 +825,8 @@ CookieService::SetCookieStringFromHttp(nsIURI* aHostURI,
RefPtr<BrowsingContext> bc = loadInfo->GetBrowsingContext();
// add the cookie to the list. AddCookie() takes care of logging.
- storage->AddCookie(crc, baseDomain, attrs, cookie, currentTimeInUsec,
- aHostURI, aCookieHeader, true, bc);
+ storage->AddCookie(crc, baseDomain, cookieOriginAttributes, cookie,
+ currentTimeInUsec, aHostURI, aCookieHeader, true, bc);
}
return NS_OK;
@@ -1180,6 +1242,16 @@ static void RecordPartitionedTelemetry(const CookieStruct& aCookieData,
}
}
+static bool HasSecurePrefix(const nsACString& aString) {
+ return StringBeginsWith(aString, "__Secure-"_ns,
+ nsCaseInsensitiveCStringComparator);
+}
+
+static bool HasHostPrefix(const nsACString& aString) {
+ return StringBeginsWith(aString, "__Host-"_ns,
+ nsCaseInsensitiveCStringComparator);
+}
+
// processes a single cookie, and returns true if there are more cookies
// to be processed
bool CookieService::CanSetCookie(
@@ -1290,9 +1362,12 @@ bool CookieService::CanSetCookie(
return newCookie;
}
- if (!CheckHiddenPrefix(aCookieData)) {
+ // If a cookie is nameless, then its value must not start with
+ // `__Host-` or `__Secure-`
+ if (aCookieData.name().IsEmpty() && (HasSecurePrefix(aCookieData.value()) ||
+ HasHostPrefix(aCookieData.value()))) {
COOKIE_LOGFAILURE(SET_COOKIE, aHostURI, savedCookieHeader,
- "failed the CheckHiddenPrefix tests");
+ "failed hidden prefix tests");
CookieLogging::LogMessageToConsole(
aCRC, aHostURI, nsIScriptError::warningFlag, CONSOLE_REJECTION_CATEGORY,
"CookieRejectedInvalidPrefix"_ns,
@@ -1449,10 +1524,9 @@ bool CookieService::CanSetCookie(
separators = ";" | "="
value-sep = ";"
cookie-sep = CR | LF
- allowed-chars = <any OCTET except NUL or cookie-sep>
+ allowed-chars = <any OCTET except cookie-sep>
OCTET = <any 8-bit sequence of data>
LWS = SP | HT
- NUL = <US-ASCII NUL, null control character (0)>
CR = <US-ASCII CR, carriage return (13)>
LF = <US-ASCII LF, linefeed (10)>
SP = <US-ASCII SP, space (32)>
@@ -1472,6 +1546,8 @@ bool CookieService::CanSetCookie(
| "Max-Age" "=" value
| "Comment" "=" value
| "Version" "=" value
+ | "Partitioned"
+ | "SameSite"
| "Secure"
| "HttpOnly"
@@ -1479,7 +1555,6 @@ bool CookieService::CanSetCookie(
// clang-format on
// helper functions for GetTokenValue
-static inline bool isnull(char c) { return c == 0; }
static inline bool iswhitespace(char c) { return c == ' ' || c == '\t'; }
static inline bool isterminator(char c) { return c == '\n' || c == '\r'; }
static inline bool isvalueseparator(char c) {
@@ -1507,7 +1582,7 @@ bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter,
++aIter;
}
start = aIter;
- while (aIter != aEndIter && !isnull(*aIter) && !istokenseparator(*aIter)) {
+ while (aIter != aEndIter && !istokenseparator(*aIter)) {
++aIter;
}
@@ -1530,7 +1605,7 @@ bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter,
// process <token>
// just look for ';' to terminate ('=' allowed)
- while (aIter != aEndIter && !isnull(*aIter) && !isvalueseparator(*aIter)) {
+ while (aIter != aEndIter && !isvalueseparator(*aIter)) {
++aIter;
}
@@ -1549,6 +1624,21 @@ bool CookieService::GetTokenValue(nsACString::const_char_iterator& aIter,
// if on terminator, increment past & return true to process new cookie
if (isterminator(*aIter)) {
++aIter;
+ while (aIter != aEndIter && isvalueseparator(*aIter)) {
+ ++aIter;
+ }
+ nsACString::const_char_iterator end = aIter - 1;
+ if (!isterminator(*end)) {
+ // The cookie isn't valid because we have multiple terminators or
+ // a terminator followed by a value separator. Add those invalid
+ // characters to the cookie string or value so it will be rejected.
+ if (aEqualsFound) {
+ aTokenString.Rebind(start, end);
+ } else {
+ aTokenValue.Rebind(start, end);
+ }
+ return false;
+ }
return true;
}
// fall-through: aIter is on ';', increment and return false
@@ -1570,6 +1660,17 @@ static inline void SetSameSiteAttribute(CookieStruct& aCookieData,
aCookieData.rawSameSite() = aValue;
}
+// Tests for control characters, defined by RFC 5234 to be %x00-1F / %x7F.
+// An exception is made for HTAB as the cookie spec treats that as whitespace.
+static bool ContainsControlChars(const nsACString& aString) {
+ const auto* start = aString.BeginReading();
+ const auto* end = aString.EndReading();
+
+ return std::find_if(start, end, [](unsigned char c) {
+ return (c <= 0x1F && c != 0x09) || c == 0x7F;
+ }) != end;
+}
+
// Parses attributes from cookie header. expires/max-age attributes aren't
// folded into the cookie struct here, because we don't know which one to use
// until we've parsed the header.
@@ -1627,6 +1728,14 @@ bool CookieService::ParseAttributes(nsIConsoleReportCollector* aCRC,
newCookie = GetTokenValue(cookieStart, cookieEnd, tokenString, tokenValue,
equalsFound);
+ if (ContainsControlChars(tokenString) || ContainsControlChars(tokenValue)) {
+ CookieLogging::LogMessageToConsole(
+ aCRC, aHostURI, nsIScriptError::errorFlag, CONSOLE_REJECTION_CATEGORY,
+ "CookieRejectedInvalidCharAttributes"_ns,
+ AutoTArray<nsString, 1>{NS_ConvertUTF8toUTF16(aCookieData.name())});
+ return newCookie;
+ }
+
// decide which attribute we have, and copy the string
if (tokenString.LowerCaseEqualsLiteral(kPath)) {
aCookieData.path() = tokenValue;
@@ -1967,25 +2076,6 @@ bool CookieService::CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI,
return true;
}
-// static
-bool CookieService::CheckHiddenPrefix(CookieStruct& aCookieData) {
- // If a cookie is nameless, then its value must not start with
- // `__Host-` or `__Secure-`
- if (!aCookieData.name().IsEmpty()) {
- return true;
- }
-
- if (StringBeginsWith(aCookieData.value(), "__Host-"_ns)) {
- return false;
- }
-
- if (StringBeginsWith(aCookieData.value(), "__Secure-"_ns)) {
- return false;
- }
-
- return true;
-}
-
namespace {
nsAutoCString GetPathFromURI(nsIURI* aHostURI) {
// strip down everything after the last slash to get the path,
@@ -2053,15 +2143,10 @@ bool CookieService::CheckPath(CookieStruct& aCookieData,
// regularized and validated the CookieStruct values!
bool CookieService::CheckPrefixes(CookieStruct& aCookieData,
bool aSecureRequest) {
- static const char kSecure[] = "__Secure-";
- static const char kHost[] = "__Host-";
- static const int kSecureLen = sizeof(kSecure) - 1;
- static const int kHostLen = sizeof(kHost) - 1;
-
- bool isSecure = strncmp(aCookieData.name().get(), kSecure, kSecureLen) == 0;
- bool isHost = strncmp(aCookieData.name().get(), kHost, kHostLen) == 0;
+ bool hasSecurePrefix = HasSecurePrefix(aCookieData.name());
+ bool hasHostPrefix = HasHostPrefix(aCookieData.name());
- if (!isSecure && !isHost) {
+ if (!hasSecurePrefix && !hasHostPrefix) {
// not one of the magic prefixes: carry on
return true;
}
@@ -2072,7 +2157,7 @@ bool CookieService::CheckPrefixes(CookieStruct& aCookieData,
return false;
}
- if (isHost) {
+ if (hasHostPrefix) {
// The host prefix requires that the path is "/" and that the cookie
// had no domain attribute. CheckDomain() and CheckPath() MUST be run
// first to make sure invalid attributes are rejected and to regularlize
diff --git a/netwerk/cookie/CookieService.h b/netwerk/cookie/CookieService.h
index 51344d6909..ca409cb12f 100644
--- a/netwerk/cookie/CookieService.h
+++ b/netwerk/cookie/CookieService.h
@@ -124,7 +124,6 @@ class CookieService final : public nsICookieService,
static bool CheckDomain(CookieStruct& aCookieData, nsIURI* aHostURI,
const nsACString& aBaseDomain,
bool aRequireHostMatch);
- static bool CheckHiddenPrefix(CookieStruct& aCookieData);
static bool CheckPath(CookieStruct& aCookieData,
nsIConsoleReportCollector* aCRC, nsIURI* aHostURI);
static bool CheckPrefixes(CookieStruct& aCookieData, bool aSecureRequest);
diff --git a/netwerk/cookie/CookieServiceChild.cpp b/netwerk/cookie/CookieServiceChild.cpp
index 84f34a9a37..dab62d1523 100644
--- a/netwerk/cookie/CookieServiceChild.cpp
+++ b/netwerk/cookie/CookieServiceChild.cpp
@@ -96,9 +96,9 @@ RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad(
aChannel->GetURI(getter_AddRefs(uri));
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
- OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ OriginAttributes storageOriginAttributes = loadInfo->GetOriginAttributes();
StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
- aChannel, attrs);
+ aChannel, storageOriginAttributes);
bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel);
bool hadCrossSiteRedirects = false;
@@ -107,11 +107,30 @@ RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad(
RefPtr<CookieServiceChild> self(this);
- // TODO (Bug 1874174): A channel could access both unpartitioned and
- // partitioned cookie jars. We will need to pass partitioned and unpartitioned
- // originAttributes according the storage access.
- nsTArray<OriginAttributes> attrsList;
- attrsList.AppendElement(attrs);
+ nsTArray<OriginAttributes> originAttributesList;
+ originAttributesList.AppendElement(storageOriginAttributes);
+
+ // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available
+ // (and therefore the partitioned OriginAttributes), the unpartitioned cookie
+ // jar is only available in first-party or third-party with storageAccess
+ // contexts.
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ bool isUnpartitioned = storageOriginAttributes.mPartitionKey.IsEmpty();
+ if (isCHIPS && isUnpartitioned) {
+ // Assert that we are only doing this if we are first-party or third-party
+ // with storageAccess.
+ MOZ_ASSERT(
+ !result.contains(ThirdPartyAnalysis::IsForeign) ||
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted));
+ // Add the partitioned principal to principals.
+ OriginAttributes partitionedOriginAttributes;
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, partitionedOriginAttributes,
+ StoragePrincipalHelper::ePartitionedPrincipal);
+ originAttributesList.AppendElement(partitionedOriginAttributes);
+ // Assert partitionedOAs have partitioneKey set.
+ MOZ_ASSERT(!partitionedOriginAttributes.mPartitionKey.IsEmpty());
+ }
return SendGetCookieList(
uri, result.contains(ThirdPartyAnalysis::IsForeign),
@@ -121,7 +140,7 @@ RefPtr<GenericPromise> CookieServiceChild::TrackCookieLoad(
result.contains(
ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
rejectedReason, isSafeTopLevelNav, isSameSiteForeign,
- hadCrossSiteRedirects, attrsList)
+ hadCrossSiteRedirects, originAttributesList)
->Then(
GetCurrentSerialEventTarget(), __func__,
[self, uri](const nsTArray<CookieStructTable>& aCookiesListTable) {
@@ -325,15 +344,6 @@ CookieServiceChild::GetCookieStringFromDocument(dom::Document* aDocument,
aCookieString.Truncate();
- nsCOMPtr<nsIPrincipal> cookiePrincipal =
- aDocument->EffectiveCookiePrincipal();
-
- // TODO (Bug 1874174): A document could access both unpartitioned and
- // partitioned cookie jars. We will need to prepare partitioned and
- // unpartitioned principals for access both cookie jars.
- nsTArray<nsCOMPtr<nsIPrincipal>> principals;
- principals.AppendElement(cookiePrincipal);
-
bool thirdParty = true;
nsPIDOMWindowInner* innerWindow = aDocument->GetInnerWindow();
// in gtests we don't have a window, let's consider those requests as 3rd
@@ -347,6 +357,26 @@ CookieServiceChild::GetCookieStringFromDocument(dom::Document* aDocument,
}
}
+ nsCOMPtr<nsIPrincipal> cookiePrincipal =
+ aDocument->EffectiveCookiePrincipal();
+
+ nsTArray<nsCOMPtr<nsIPrincipal>> principals;
+ principals.AppendElement(cookiePrincipal);
+
+ // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available
+ // (and therefore the partitioned principal), the unpartitioned cookie jar is
+ // only available in first-party or third-party with storageAccess contexts.
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ bool isUnpartitioned =
+ cookiePrincipal->OriginAttributesRef().mPartitionKey.IsEmpty();
+ if (isCHIPS && isUnpartitioned) {
+ // Assert that we are only doing this if we are first-party or third-party
+ // with storageAccess.
+ MOZ_ASSERT(!thirdParty || aDocument->UsingStorageAccess());
+ // Add the partitioned principal to principals
+ principals.AppendElement(aDocument->PartitionedPrincipal());
+ }
+
for (auto& principal : principals) {
if (!CookieCommons::IsSchemeSupported(principal)) {
return NS_OK;
@@ -488,7 +518,14 @@ CookieServiceChild::SetCookieStringFromDocument(
// (those come from the parent, which already checks this),
// but script could see an inconsistent view of things.
- nsCOMPtr<nsIPrincipal> principal = aDocument->EffectiveCookiePrincipal();
+ // CHIPS - If the cookie has the "Partitioned" attribute set it will be
+ // stored in the partitioned cookie jar.
+ bool needPartitioned =
+ StaticPrefs::network_cookie_cookieBehavior_optInPartitioning() &&
+ cookie->RawIsPartitioned();
+ nsCOMPtr<nsIPrincipal> principal =
+ needPartitioned ? aDocument->PartitionedPrincipal()
+ : aDocument->EffectiveCookiePrincipal();
bool isPotentiallyTrustworthy =
principal->GetIsOriginPotentiallyTrustworthy();
@@ -560,9 +597,10 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI,
nsCString cookieString(aCookieString);
- OriginAttributes attrs = loadInfo->GetOriginAttributes();
+ OriginAttributes storagePrincipalOriginAttributes =
+ loadInfo->GetOriginAttributes();
StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
- aChannel, attrs);
+ aChannel, storagePrincipalOriginAttributes);
bool requireHostMatch;
nsCString baseDomain;
@@ -580,18 +618,15 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI,
result.contains(ThirdPartyAnalysis::IsThirdPartyTrackingResource),
result.contains(ThirdPartyAnalysis::IsThirdPartySocialTrackingResource),
result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted),
- aCookieString, CountCookiesFromHashTable(baseDomain, attrs), attrs,
- &rejectedReason);
+ aCookieString,
+ CountCookiesFromHashTable(baseDomain, storagePrincipalOriginAttributes),
+ storagePrincipalOriginAttributes, &rejectedReason);
if (cookieStatus != STATUS_ACCEPTED &&
cookieStatus != STATUS_ACCEPT_SESSION) {
return NS_OK;
}
- CookieKey key(baseDomain, attrs);
-
- nsTArray<CookieStruct> cookiesToSend;
-
int64_t currentTimeInUsec = PR_Now();
bool addonAllowsLoad = false;
@@ -612,6 +647,23 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI,
nsICookieService::BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN &&
!result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted);
+ // CHIPS - The partitioned cookie jar is always available and it is always
+ // possible to store cookies in it using the "Partitioned" attribute.
+ // Prepare the partitioned principals OAs to enable possible partitioned
+ // cookie storing from first-party or with StorageAccess.
+ // Similar behavior to CookieService::SetCookieStringFromHttp().
+ OriginAttributes partitionedPrincipalOriginAttributes;
+ bool isPartitionedPrincipal =
+ !storagePrincipalOriginAttributes.mPartitionKey.IsEmpty();
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ // Only need to get OAs if we don't already use the partitioned principal.
+ if (isCHIPS && !isPartitionedPrincipal) {
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, partitionedPrincipalOriginAttributes,
+ StoragePrincipalHelper::ePartitionedPrincipal);
+ }
+
+ nsTArray<CookieStruct> cookiesToSend, partitionedCookiesToSend;
bool moreCookies;
do {
CookieStruct cookieData;
@@ -642,23 +694,47 @@ CookieServiceChild::SetCookieStringFromHttp(nsIURI* aHostURI,
continue;
}
- RefPtr<Cookie> cookie = Cookie::Create(cookieData, attrs);
+ // CHIPS - If the partitioned attribute is set, store cookie in partitioned
+ // cookie jar independent of context. If the cookies are stored in the
+ // partitioned cookie jar anyway no special treatment of CHIPS cookies
+ // necessary.
+ bool needPartitioned =
+ isCHIPS && cookieData.isPartitioned() && !isPartitionedPrincipal;
+ nsTArray<CookieStruct>& cookiesToSendRef =
+ needPartitioned ? partitionedCookiesToSend : cookiesToSend;
+ OriginAttributes& cookieOriginAttributes =
+ needPartitioned ? partitionedPrincipalOriginAttributes
+ : storagePrincipalOriginAttributes;
+ // Assert that partitionedPrincipalOriginAttributes are initialized if used.
+ MOZ_ASSERT_IF(
+ needPartitioned,
+ !partitionedPrincipalOriginAttributes.mPartitionKey.IsEmpty());
+
+ RefPtr<Cookie> cookie = Cookie::Create(cookieData, cookieOriginAttributes);
MOZ_ASSERT(cookie);
cookie->SetLastAccessed(currentTimeInUsec);
cookie->SetCreationTime(
Cookie::GenerateUniqueCreationTime(currentTimeInUsec));
- RecordDocumentCookie(cookie, attrs);
- cookiesToSend.AppendElement(cookieData);
+ RecordDocumentCookie(cookie, cookieOriginAttributes);
+ cookiesToSendRef.AppendElement(cookieData);
} while (moreCookies);
// Asynchronously call the parent.
- if (CanSend() && !cookiesToSend.IsEmpty()) {
+ if (CanSend()) {
RefPtr<HttpChannelChild> httpChannelChild = do_QueryObject(aChannel);
MOZ_ASSERT(httpChannelChild);
- httpChannelChild->SendSetCookies(baseDomain, attrs, aHostURI, true,
- cookiesToSend);
+ if (!cookiesToSend.IsEmpty()) {
+ httpChannelChild->SendSetCookies(baseDomain,
+ storagePrincipalOriginAttributes,
+ aHostURI, true, cookiesToSend);
+ }
+ if (!partitionedCookiesToSend.IsEmpty()) {
+ httpChannelChild->SendSetCookies(
+ baseDomain, partitionedPrincipalOriginAttributes, aHostURI, true,
+ partitionedCookiesToSend);
+ }
}
return NS_OK;
diff --git a/netwerk/cookie/CookieServiceParent.cpp b/netwerk/cookie/CookieServiceParent.cpp
index 75c024cec6..187a31e002 100644
--- a/netwerk/cookie/CookieServiceParent.cpp
+++ b/netwerk/cookie/CookieServiceParent.cpp
@@ -15,6 +15,7 @@
#include "mozIThirdPartyUtil.h"
#include "nsArrayUtils.h"
#include "nsIChannel.h"
+#include "mozilla/StaticPrefs_network.h"
#include "nsIEffectiveTLDService.h"
#include "nsNetCID.h"
#include "nsMixedContentBlocker.h"
@@ -119,17 +120,11 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
aChannel->GetURI(getter_AddRefs(uri));
nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo();
- OriginAttributes attrs = loadInfo->GetOriginAttributes();
bool isSafeTopLevelNav = CookieCommons::IsSafeTopLevelNav(aChannel);
bool hadCrossSiteRedirects = false;
bool isSameSiteForeign =
CookieCommons::IsSameSiteForeign(aChannel, uri, &hadCrossSiteRedirects);
- // TODO (Bug 1874174): A channel could load both unpartitioned and partitioned
- // cookie jars together. We will need to track both originAttributes for them.
- StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
- aChannel, attrs);
-
nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil;
thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
@@ -137,8 +132,34 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
ThirdPartyAnalysisResult result = thirdPartyUtil->AnalyzeChannel(
aChannel, false, nullptr, nullptr, &rejectedReason);
+ OriginAttributes storageOriginAttributes = loadInfo->GetOriginAttributes();
+ StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
+ aChannel, storageOriginAttributes);
+
nsTArray<OriginAttributes> originAttributesList;
- originAttributesList.AppendElement(attrs);
+ originAttributesList.AppendElement(storageOriginAttributes);
+
+ // CHIPS - If CHIPS is enabled the partitioned cookie jar is always available
+ // (and therefore the partitioned OriginAttributes), the unpartitioned cookie
+ // jar is only available in first-party or third-party with storageAccess
+ // contexts.
+ bool isCHIPS = StaticPrefs::network_cookie_cookieBehavior_optInPartitioning();
+ bool isUnpartitioned = storageOriginAttributes.mPartitionKey.IsEmpty();
+ if (isCHIPS && isUnpartitioned) {
+ // Assert that we are only doing this if we are first-party or third-party
+ // with storageAccess.
+ MOZ_ASSERT(
+ !result.contains(ThirdPartyAnalysis::IsForeign) ||
+ result.contains(ThirdPartyAnalysis::IsStorageAccessPermissionGranted));
+ // Add the partitioned principal to principals
+ OriginAttributes partitionedOriginAttributes;
+ StoragePrincipalHelper::GetOriginAttributes(
+ aChannel, partitionedOriginAttributes,
+ StoragePrincipalHelper::ePartitionedPrincipal);
+ originAttributesList.AppendElement(partitionedOriginAttributes);
+ // Assert partitionedOAs have partitioneKey set.
+ MOZ_ASSERT(!partitionedOriginAttributes.mPartitionKey.IsEmpty());
+ }
for (auto& originAttributes : originAttributesList) {
UpdateCookieInContentList(uri, originAttributes);
diff --git a/netwerk/cookie/test/browser/browser.toml b/netwerk/cookie/test/browser/browser.toml
index 05a302ddbd..56807e4bf1 100644
--- a/netwerk/cookie/test/browser/browser.toml
+++ b/netwerk/cookie/test/browser/browser.toml
@@ -8,6 +8,9 @@ support-files = [
["browser_broadcastChannel.js"]
+["browser_cookie_chips.js"]
+support-files = ["chips.sjs"]
+
["browser_cookie_insecure_overwrites_secure.js"]
["browser_cookie_purge_sync.js"]
@@ -17,6 +20,9 @@ support-files = ["server.sjs"]
["browser_cookies_ipv6.js"]
+["browser_cookies_serviceWorker.js"]
+support-files = ["cookies.sjs", "serviceWorker.js"]
+
["browser_domCache.js"]
["browser_indexedDB.js"]
diff --git a/netwerk/cookie/test/browser/browser_cookie_chips.js b/netwerk/cookie/test/browser/browser_cookie_chips.js
new file mode 100644
index 0000000000..ba0c8f3a0e
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_cookie_chips.js
@@ -0,0 +1,539 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(() => {
+ // These are functional and not integration tests, cookieBehavior accept lets
+ // us have StorageAccess on thirparty.
+ Services.prefs.setIntPref(
+ "network.cookie.cookieBehavior",
+ Ci.nsICookieService.BEHAVIOR_ACCEPT
+ );
+ Services.prefs.setBoolPref(
+ "network.cookieJarSettings.unblocked_for_testing",
+ true
+ );
+ Services.prefs.setBoolPref(
+ "network.cookie.cookieBehavior.optInPartitioning",
+ true
+ );
+ Services.prefs.setBoolPref("dom.storage_access.enabled", true);
+ Services.prefs.setBoolPref("dom.storage_access.prompt.testing", true);
+ Services.cookies.removeAll();
+ Services.perms.removeAll();
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+ Services.prefs.clearUserPref(
+ "network.cookieJarSettings.unblocked_for_testing"
+ );
+ Services.prefs.clearUserPref(
+ "network.cookie.cookieBehavior.optInPartitioning"
+ );
+ Services.prefs.clearUserPref("dom.storage_access.enabled");
+ Services.prefs.clearUserPref("dom.storage_access.prompt.testing");
+ Services.cookies.removeAll();
+ Services.perms.removeAll();
+});
+
+const COOKIE_PARTITIONED =
+ "cookie=partitioned; Partitioned; Secure; SameSite=None;";
+const COOKIE_UNPARTITIONED = "cookie=unpartitioned; Secure; SameSite=None;";
+
+const PATH = "/browser/netwerk/cookie/test/browser/";
+const PATH_EMPTY = PATH + "file_empty.html";
+const HTTP_COOKIE_SET = PATH + "chips.sjs?set";
+const HTTP_COOKIE_GET = PATH + "chips.sjs?get";
+
+const FIRST_PARTY = "example.com";
+const THIRD_PARTY = "example.org";
+
+const URL_DOCUMENT_FIRSTPARTY = "https://" + FIRST_PARTY + PATH_EMPTY;
+const URL_DOCUMENT_THIRDPARTY = "https://" + THIRD_PARTY + PATH_EMPTY;
+const URL_HTTP_FIRSTPARTY = "https://" + FIRST_PARTY + "/" + HTTP_COOKIE_SET;
+const URL_HTTP_THIRDPARTY = "https://" + THIRD_PARTY + "/" + HTTP_COOKIE_SET;
+
+function createOriginAttributes(partitionKey) {
+ return JSON.stringify({
+ firstPartyDomain: "",
+ geckoViewSessionContextId: "",
+ inIsolatedMozBrowser: false,
+ partitionKey,
+ privateBrowsingId: 0,
+ userContextId: 0,
+ });
+}
+
+function createPartitonKey(url) {
+ let uri = NetUtil.newURI(url);
+ return `(${uri.scheme},${uri.host})`;
+}
+
+// OriginAttributes used to access partitioned and unpartitioned cookie jars
+// in all tests.
+const partitionedOAs = createOriginAttributes(
+ createPartitonKey(URL_DOCUMENT_FIRSTPARTY)
+);
+const unpartitionedOAs = createOriginAttributes("");
+
+// Set partitioned and unpartitioned cookie from first-party document.
+// CHIPS "Partitioned" cookie MUST always be stored in partitioned jar.
+// This calls CookieServiceChild::SetCookieStringFromDocument() internally.
+// CookieService::SetCookieStringFromDocument() is not explicitly tested since
+// CHIPS are in the common function CookieCommons::CreateCookieFromDocument().
+add_task(
+ async function test_chips_store_partitioned_document_first_party_child() {
+ const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Set partitioned and unpartitioned cookie from document child-side
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_PARTITIONED, COOKIE_UNPARTITIONED],
+ (partitioned, unpartitioned) => {
+ content.document.cookie = partitioned;
+ content.document.cookie = unpartitioned;
+ }
+ );
+
+ // Get cookies from partitioned jar
+ let partitioned = Services.cookies.getCookiesWithOriginAttributes(
+ partitionedOAs,
+ FIRST_PARTY
+ );
+ // Get cookies from unpartitioned jar
+ let unpartitioned = Services.cookies.getCookiesWithOriginAttributes(
+ unpartitionedOAs,
+ FIRST_PARTY
+ );
+
+ // Assert partitioned/unpartitioned cookie were stored in correct jars
+ Assert.equal(partitioned.length, 1);
+ Assert.equal(partitioned[0].value, "partitioned");
+ Assert.equal(unpartitioned.length, 1);
+ Assert.equal(unpartitioned[0].value, "unpartitioned");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ }
+);
+
+// Set partitioned and unpartitioned cookie from third-party document with storage
+// access. CHIPS "Partitioned" cookie MUST always be stored in partitioned jar.
+// This calls CookieServiceChild::SetCookieStringFromDocument() internally.
+// CookieService::SetCookieStringFromDocument() is not explicitly tested since
+// CHIPS are in the common function CookieCommons::CreateCookieFromDocument().
+add_task(
+ async function test_chips_store_partitioned_document_third_party_storage_access_child() {
+ const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Spawn document bc
+ await SpecialPowers.spawn(
+ browser,
+ [URL_DOCUMENT_THIRDPARTY, COOKIE_PARTITIONED, COOKIE_UNPARTITIONED],
+ async (url, partitioned, unpartitioned) => {
+ let ifr = content.document.createElement("iframe");
+ ifr.src = url;
+ content.document.body.appendChild(ifr);
+ await ContentTaskUtils.waitForEvent(ifr, "load");
+
+ // Spawn iframe bc
+ await SpecialPowers.spawn(
+ await ifr.browsingContext,
+ [partitioned, unpartitioned],
+ async (partitioned, unpartitioned) => {
+ ok(
+ await content.document.hasStorageAccess(),
+ "example.org should have storageAccess by CookieBehavior 0 / test setup"
+ );
+
+ content.document.cookie = partitioned;
+ content.document.cookie = unpartitioned;
+ }
+ );
+ }
+ );
+
+ // Get cookies from partitioned jar
+ let partitioned = Services.cookies.getCookiesWithOriginAttributes(
+ partitionedOAs,
+ THIRD_PARTY
+ );
+ // Get cookies from unpartitioned jar
+ let unpartitioned = Services.cookies.getCookiesWithOriginAttributes(
+ unpartitionedOAs,
+ THIRD_PARTY
+ );
+
+ // Assert partitioned/unpartitioned cookie were stored in correct jars
+ Assert.equal(partitioned.length, 1);
+ Assert.equal(partitioned[0].value, "partitioned");
+ Assert.equal(unpartitioned.length, 1);
+ Assert.equal(unpartitioned[0].value, "unpartitioned");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ }
+);
+
+// Set partitioned and unpartitioned cookie from first-party http load.
+// CHIPS "Partitioned" cookie MUST always be stored in partitioned jar.
+// This calls CookieService::SetCookieStringFromHttp() internally.
+add_task(async function test_chips_store_partitioned_http_first_party_parent() {
+ // Set partitioned and unpartitioned cookie from http parent side through
+ // chips.sjs being loaded.
+ const tab = BrowserTestUtils.addTab(gBrowser, URL_HTTP_FIRSTPARTY);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Get cookies from partitioned jar
+ let partitioned = Services.cookies.getCookiesWithOriginAttributes(
+ partitionedOAs,
+ FIRST_PARTY
+ );
+ // Get cookies from unpartitioned jar
+ let unpartitioned = Services.cookies.getCookiesWithOriginAttributes(
+ unpartitionedOAs,
+ FIRST_PARTY
+ );
+
+ // Assert partitioned/unpartitioned cookie were stored in correct jars
+ Assert.equal(partitioned.length, 1);
+ Assert.equal(partitioned[0].value, "partitioned");
+ Assert.equal(unpartitioned.length, 1);
+ Assert.equal(unpartitioned[0].value, "unpartitioned");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+});
+
+// Set partitioned and unpartitioned cookie from third-party http load.
+// CHIPS "Partitioned" cookie MUST always be stored in partitioned jar.
+// This calls CookieService::SetCookieStringFromHttp() internally.
+add_task(
+ async function test_chips_store_partitioned_http_third_party_storage_access_parent() {
+ const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Spawn document bc
+ await SpecialPowers.spawn(browser, [URL_HTTP_THIRDPARTY], async url => {
+ let ifr = content.document.createElement("iframe");
+ ifr.src = url;
+ content.document.body.appendChild(ifr);
+ // Send http request with "set" query parameter, partitioned and
+ // unpartitioned cookie will be set through http response from chips.sjs.
+ await ContentTaskUtils.waitForEvent(ifr, "load");
+
+ // Spawn iframe bc
+ await SpecialPowers.spawn(await ifr.browsingContext, [], async () => {
+ ok(
+ await content.document.hasStorageAccess(),
+ "example.org should have storageAccess by CookieBehavior 0 / test setup"
+ );
+ });
+ });
+
+ // Get cookies from partitioned jar
+ let partitioned = Services.cookies.getCookiesWithOriginAttributes(
+ partitionedOAs,
+ THIRD_PARTY
+ );
+ // Get cookies from unpartitioned jar
+ let unpartitioned = Services.cookies.getCookiesWithOriginAttributes(
+ unpartitionedOAs,
+ THIRD_PARTY
+ );
+
+ // Assert partitioned/unpartitioned cookie were stored in correct jars
+ Assert.equal(partitioned.length, 1);
+ Assert.equal(partitioned[0].value, "partitioned");
+ Assert.equal(unpartitioned.length, 1);
+ Assert.equal(unpartitioned[0].value, "unpartitioned");
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ }
+);
+
+// TODO CHIPS - Tests for CookieServiceChild::SetCookieStringFromHttp() need
+// to be added. Since this is only checkable on onProxyConnectSuccess needs
+// proxy setup test harness. It is also called after onStartRequest() (Http)
+// but cookies are already set by the parents
+// CookieService::SetCookieStringFromHttp() call.
+
+// Get partitioned and unpartitioned cookies from document (child).
+// This calls CookieServiceChild::GetCookieStringFromDocument() internally.
+add_task(
+ async function test_chips_send_partitioned_and_unpartitioned_document_child() {
+ const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Spawn document bc
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_PARTITIONED, COOKIE_UNPARTITIONED],
+ async (partitioned, unpartitioned) => {
+ content.document.cookie = partitioned;
+ content.document.cookie = unpartitioned;
+
+ // Assert both unpartitioned and partitioned cookie are returned.
+ let cookies = content.document.cookie;
+ ok(
+ cookies.includes("cookie=partitioned"),
+ "Cookie from partitioned jar was sent."
+ );
+ ok(
+ cookies.includes("cookie=unpartitioned"),
+ "Cookie from unpartitioned jar was sent."
+ );
+ }
+ );
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ }
+);
+
+// Get partitioned and unpartitioned cookies from document (child) after
+// storageAccess was granted. This calls CookieServiceChild::TrackCookieLoad()
+// internally to update child's cookies.
+add_task(
+ async function test_chips_send_partitioned_and_unpartitioned_on_storage_access_child() {
+ // Set cookieBehavior to BEHAVIOR_REJECT_TRACKERS_AND_PARTITION_FOREIGN for
+ // requestStorageAccess() based test.
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 5);
+
+ // Set example.org first-party unpartitioned cookie
+ await BrowserTestUtils.withNewTab(
+ URL_DOCUMENT_THIRDPARTY,
+ async browser => {
+ info("Set a first party cookie via `document.cookie`.");
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_UNPARTITIONED],
+ async unpartitioned => {
+ content.document.cookie = unpartitioned;
+ is(
+ content.document.cookie,
+ "cookie=unpartitioned",
+ "Unpartitioned cookie was set."
+ );
+ }
+ );
+ }
+ );
+
+ // Assert cookie was set on parent cookie service for example.org.
+ // Get cookies from unpartitioned jar
+ let unpartitioned = Services.cookies.getCookiesWithOriginAttributes(
+ unpartitionedOAs,
+ THIRD_PARTY
+ );
+ Assert.equal(unpartitioned.length, 1);
+ Assert.equal(unpartitioned[0].value, "unpartitioned");
+
+ // Load example.com as first-party in tab
+ const tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY);
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ // Set third-party cookie from example.org iframe, get storageAccess and
+ // check cookies.
+ // Spawn document bc
+ await SpecialPowers.spawn(
+ browser,
+ [URL_DOCUMENT_THIRDPARTY, COOKIE_PARTITIONED],
+ async (url, partitioned) => {
+ // Create third-party iframe
+ let ifr = content.document.createElement("iframe");
+ ifr.src = url;
+ content.document.body.appendChild(ifr);
+ await ContentTaskUtils.waitForEvent(ifr, "load");
+
+ // Spawn iframe bc
+ await SpecialPowers.spawn(
+ await ifr.browsingContext,
+ [partitioned],
+ async partitioned => {
+ ok(
+ !(await content.document.hasStorageAccess()),
+ "example.org should not have storageAccess initially."
+ );
+
+ // Set a partitioned third-party cookie and assert its the only.
+ content.document.cookie = partitioned;
+ is(
+ content.document.cookie,
+ "cookie=partitioned",
+ "Partitioned cookie was set."
+ );
+
+ info("Simulate user activation.");
+ SpecialPowers.wrap(content.document).notifyUserGestureActivation();
+
+ info("Request storage access.");
+ await content.document.requestStorageAccess();
+
+ ok(
+ await content.document.hasStorageAccess(),
+ "example.org should now have storageAccess."
+ );
+
+ // Assert both unpartitioned and partitioned cookie are returned.
+ let cookies = content.document.cookie;
+ ok(
+ cookies.includes("cookie=partitioned"),
+ "Cookie from partitioned jar was sent."
+ );
+ ok(
+ cookies.includes("cookie=unpartitioned"),
+ "Cookie from unpartitioned jar was sent."
+ );
+ }
+ );
+ }
+ );
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ Services.perms.removeAll();
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+ }
+);
+
+// Set partitioned and unpartitioned cookies for URL_DOCUMENT_FIRSTPARTY, then
+// load URL again, assure cookies are correctly send to content child process.
+// This tests CookieServiceParent::TrackCookieLoad() internally.
+add_task(
+ async function test_chips_send_partitioned_and_unpartitioned_document_parent() {
+ // Set example.com first-party unpartitioned and partitioned cookie, then
+ // close tab.
+ await BrowserTestUtils.withNewTab(
+ URL_DOCUMENT_FIRSTPARTY,
+ async browser => {
+ await SpecialPowers.spawn(
+ browser,
+ [COOKIE_PARTITIONED, COOKIE_UNPARTITIONED],
+ async (partitioned, unpartitioned) => {
+ content.document.cookie = unpartitioned;
+ content.document.cookie = partitioned;
+ let cookies = content.document.cookie;
+ ok(
+ cookies.includes("cookie=unpartitioned"),
+ "Unpartitioned cookie was set."
+ );
+ ok(
+ cookies.includes("cookie=partitioned"),
+ "Partitioned cookie was set."
+ );
+ }
+ );
+ }
+ );
+
+ // Assert we have one partitioned and one unpartitioned cookie set.
+ // Get cookies from partitioned jar
+ let partitioned = Services.cookies.getCookiesWithOriginAttributes(
+ partitionedOAs,
+ FIRST_PARTY
+ );
+ // Get cookies from unpartitioned jar
+ let unpartitioned = Services.cookies.getCookiesWithOriginAttributes(
+ unpartitionedOAs,
+ FIRST_PARTY
+ );
+ Assert.equal(partitioned.length, 1);
+ Assert.equal(partitioned[0].value, "partitioned");
+ Assert.equal(unpartitioned.length, 1);
+ Assert.equal(unpartitioned[0].value, "unpartitioned");
+
+ // Reload example.com and assert previously set cookies are correctly
+ // send to content child document.
+ await BrowserTestUtils.withNewTab(
+ URL_DOCUMENT_FIRSTPARTY,
+ async browser => {
+ await SpecialPowers.spawn(browser, [], () => {
+ let cookies = content.document.cookie;
+ ok(
+ cookies.includes("cookie=unpartitioned"),
+ "Unpartitioned cookie was sent."
+ );
+ ok(
+ cookies.includes("cookie=partitioned"),
+ "Partitioned cookie was sent."
+ );
+ });
+ }
+ );
+
+ // Cleanup
+ Services.cookies.removeAll();
+ }
+);
+
+// Set partitioned and unpartitioned cookies for URL_DOCUMENT_FIRSTPARTY, then
+// send http request, assure cookies are correctly send in "Cookie" header.
+// This tests CookieService::GetCookieStringFromHttp() internally.
+add_task(
+ async function test_chips_send_partitioned_and_unpartitioned_http_parent() {
+ // Load empty document.
+ let tab = BrowserTestUtils.addTab(gBrowser, URL_DOCUMENT_FIRSTPARTY);
+ let browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+
+ await SpecialPowers.spawn(
+ browser,
+ [HTTP_COOKIE_SET, HTTP_COOKIE_GET],
+ async (set, get) => {
+ // Send http request with "set" query parameter, partitioned and
+ // unpartitioned cookie will be set through http response.
+ await content.fetch(set);
+
+ // Assert cookies were set to document.
+ let cookies = content.document.cookie;
+ ok(
+ cookies.includes("cookie=unpartitioned"),
+ "Unpartitioned cookie was set to document."
+ );
+ ok(
+ cookies.includes("cookie=partitioned"),
+ "Partitioned cookie was set to document."
+ );
+
+ // Send http request with "get" query parameter, chips.sjs will return
+ // the request "Cookie" header string.
+ await content
+ .fetch(get)
+ .then(response => response.text())
+ .then(requestCookies => {
+ // Assert cookies were sent in http request.
+ ok(
+ requestCookies.includes("cookie=unpartitioned"),
+ "Unpartitioned cookie was sent in http request."
+ );
+ ok(
+ requestCookies.includes("cookie=partitioned"),
+ "Partitioned cookie was sent in http request."
+ );
+ });
+ }
+ );
+
+ // Cleanup
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+ }
+);
diff --git a/netwerk/cookie/test/browser/browser_cookies_serviceWorker.js b/netwerk/cookie/test/browser/browser_cookies_serviceWorker.js
new file mode 100644
index 0000000000..813e1fe7cb
--- /dev/null
+++ b/netwerk/cookie/test/browser/browser_cookies_serviceWorker.js
@@ -0,0 +1,540 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_CROSS_SITE_DOMAIN = "https://example.net/";
+const TEST_CROSS_SITE_PAGE =
+ TEST_CROSS_SITE_DOMAIN + TEST_PATH + "file_empty.html";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true],
+ ],
+ });
+
+ Services.cookies.removeAll();
+});
+
+function registerSW(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ let reg = await content.navigator.serviceWorker.register(
+ "serviceWorker.js"
+ );
+
+ await ContentTaskUtils.waitForCondition(() => {
+ return reg.active && reg.active.state === "activated";
+ }, "The service worker is activated");
+
+ ok(
+ content.navigator.serviceWorker.controller,
+ "The service worker controls the document successfully."
+ );
+ });
+}
+
+function fetchCookiesFromSW(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ return new content.Promise(resolve => {
+ content.navigator.serviceWorker.addEventListener("message", event => {
+ resolve(event.data.content);
+ });
+
+ content.navigator.serviceWorker.controller.postMessage({
+ action: "fetch",
+ url: `cookies.sjs`,
+ });
+ });
+ });
+}
+
+function setCookiesFromSW(browser, cookies) {
+ let setCookieQuery = "";
+
+ for (let cookie of cookies) {
+ setCookieQuery += `Set-Cookie=${cookie}&`;
+ }
+
+ return SpecialPowers.spawn(browser, [setCookieQuery], async query => {
+ return new content.Promise(resolve => {
+ content.navigator.serviceWorker.addEventListener("message", event => {
+ resolve(event.data.content);
+ });
+
+ content.navigator.serviceWorker.controller.postMessage({
+ action: "fetch",
+ url: `cookies.sjs?${query}`,
+ });
+ });
+ });
+}
+
+function unregisterSW(browser) {
+ return SpecialPowers.spawn(browser, [], async _ => {
+ const regs = await content.navigator.serviceWorker.getRegistrations();
+ for (const reg of regs) {
+ await reg.unregister();
+ }
+ });
+}
+
+/**
+ * Verify a first-party service worker can access both SameSite=None and
+ * SameSite=Lax cookies set in the first-party context.
+ */
+add_task(async function testCookiesWithFirstPartyServiceWorker() {
+ info("Open a tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info("Writing cookies to the first-party context.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const cookies = [
+ "foo=bar; SameSite=None; Secure",
+ "fooLax=barLax; SameSite=Lax; Secure",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info("Register a service worker and trigger a fetch request to get cookies.");
+ await registerSW(tab.linkedBrowser);
+ let cookieStr = await fetchCookiesFromSW(tab.linkedBrowser);
+
+ is(cookieStr, "foo=bar; fooLax=barLax", "The cookies are expected");
+
+ info("Set cookies from the service worker.");
+ await setCookiesFromSW(tab.linkedBrowser, [
+ "foo=barSW; SameSite=None; Secure",
+ "fooLax=barLaxSW; SameSite=Lax; Secure",
+ ]);
+
+ info("Get cookies from the service worker.");
+ cookieStr = await fetchCookiesFromSW(tab.linkedBrowser);
+
+ is(cookieStr, "foo=barSW; fooLax=barLaxSW", "The cookies are expected");
+
+ await unregisterSW(tab.linkedBrowser);
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+});
+
+/**
+ * Verify a cross-site service worker can only access cookies set in the
+ * same cross-site context.
+ */
+add_task(async function testCookiesWithCrossSiteServiceWorker() {
+ // Disable blocking third-party cookies.
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", false]],
+ });
+
+ info("Open a cross-site tab");
+ let crossSiteTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_CROSS_SITE_PAGE
+ );
+
+ info("Writing cookies to the cross site in the first-party context.");
+ await SpecialPowers.spawn(crossSiteTab.linkedBrowser, [], async _ => {
+ const cookies = [
+ "foo=bar; SameSite=None; Secure",
+ "fooLax=barLax; SameSite=Lax; Secure",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info("Open a tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info("Load a cross-site iframe");
+ let crossSiteBc = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_CROSS_SITE_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ return ifr.browsingContext;
+ }
+ );
+
+ info("Write cookies in the cross-site iframe");
+ await SpecialPowers.spawn(crossSiteBc, [], async _ => {
+ const cookies = [
+ "foo=crossBar; SameSite=None; Secure",
+ "fooLax=crossBarLax; SameSite=Lax; Secure",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info(
+ "Register a service worker and trigger a fetch request to get cookies in cross-site context."
+ );
+ await registerSW(crossSiteBc);
+ let cookieStr = await fetchCookiesFromSW(crossSiteBc);
+
+ is(
+ cookieStr,
+ "foo=crossBar",
+ "Only the SameSite=None cookie set in the third-party iframe is available."
+ );
+
+ info("Set cookies from the third-party service worker.");
+ await setCookiesFromSW(crossSiteBc, [
+ "foo=crossBarSW; SameSite=None; Secure",
+ "fooLax=crossBarLaxSW; SameSite=Lax; Secure",
+ ]);
+
+ info("Get cookies from the third-party service worker.");
+ cookieStr = await fetchCookiesFromSW(crossSiteBc);
+
+ is(
+ cookieStr,
+ "foo=crossBarSW",
+ "Only the SameSite=None cookie set in the third-party service worker is available."
+ );
+
+ await unregisterSW(crossSiteBc);
+ BrowserTestUtils.removeTab(crossSiteTab);
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+});
+
+/**
+ * Verify a cross-site service worker can only access partitioned cookies set in
+ * the same cross-site context if third-party cookies are blocked.
+ */
+add_task(async function testPartitionedCookiesWithCrossSiteServiceWorker() {
+ // Enable blocking third-party cookies.
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", true]],
+ });
+
+ info("Open a tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info("Load a cross-site iframe");
+ let crossSiteBc = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_CROSS_SITE_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ return ifr.browsingContext;
+ }
+ );
+
+ info("Write cookies in the cross-site iframe");
+ await SpecialPowers.spawn(crossSiteBc, [], async _ => {
+ const cookies = [
+ "foo=crossBar; SameSite=None; Secure",
+ "fooLax=crossBarLax; SameSite=Lax; Secure",
+ "fooPartitioned=crossBar; SameSite=None; Secure; Partitioned;",
+ "fooLaxPartitioned=crossBarLax; SameSite=Lax; Secure; Partitioned;",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info(
+ "Register a service worker and trigger a fetch request to get cookies in cross-site context."
+ );
+ await registerSW(crossSiteBc);
+ let cookieStr = await fetchCookiesFromSW(crossSiteBc);
+
+ is(
+ cookieStr,
+ "fooPartitioned=crossBar",
+ "Only the SameSite=None partitioned cookie set in the third-party iframe is available."
+ );
+
+ info("Set cookies from the third-party service worker.");
+ await setCookiesFromSW(crossSiteBc, [
+ "foo=crossBarSW; SameSite=None; Secure",
+ "fooLax=crossBarLaxSW; SameSite=Lax; Secure",
+ "fooPartitioned=crossBarSW; SameSite=None; Secure; Partitioned;",
+ "fooLaxPartitioned=crossBarLaxSW; SameSite=Lax; Secure; Partitioned;",
+ ]);
+
+ info("Get cookies from the third-party service worker.");
+ cookieStr = await fetchCookiesFromSW(crossSiteBc);
+
+ is(
+ cookieStr,
+ "fooPartitioned=crossBarSW",
+ "Only the SameSite=None partitioned cookie set in the third-party service worker is available."
+ );
+
+ await unregisterSW(crossSiteBc);
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+});
+
+/**
+ * Verify a ABA service worker can only access cookies set in the ABA context.
+ */
+add_task(async function testCookiesWithABAServiceWorker() {
+ // Disable blocking third-party cookies.
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", false]],
+ });
+
+ info("Open a tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info("Writing cookies to the first-party context.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const cookies = [
+ "foo=bar; SameSite=None; Secure",
+ "fooLax=barLax; SameSite=Lax; Secure",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info("Load a ABA iframe");
+ let crossSiteBc = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_CROSS_SITE_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ return ifr.browsingContext;
+ }
+ );
+
+ let ABABc = await SpecialPowers.spawn(
+ crossSiteBc,
+ [TEST_TOP_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ return ifr.browsingContext;
+ }
+ );
+
+ info(
+ "Register a service worker and trigger a fetch request to get cookies in the ABA context."
+ );
+
+ await registerSW(ABABc);
+ let cookieStr = await fetchCookiesFromSW(ABABc);
+ is(cookieStr, "", "No cookie should be available in ABA context.");
+
+ info("Set cookies in the ABA iframe");
+ await SpecialPowers.spawn(ABABc, [], async _ => {
+ const cookies = [
+ "fooABA=barABA; SameSite=None; Secure",
+ "fooABALax=BarABALax; SameSite=Lax; Secure",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info("Get cookies in the ABA service worker.");
+ cookieStr = await fetchCookiesFromSW(ABABc);
+
+ is(
+ cookieStr,
+ "fooABA=barABA",
+ "Only the SameSite=None cookie set in ABA iframe is available."
+ );
+
+ info("Set cookies from the service worker in ABA context");
+ await setCookiesFromSW(ABABc, [
+ "fooABA=barABASW; SameSite=None; Secure",
+ "fooABALax=BarABALaxSW; SameSite=Lax; Secure",
+ ]);
+
+ info("Get cookies from the service worker in the ABA context.");
+ cookieStr = await fetchCookiesFromSW(ABABc);
+
+ is(
+ cookieStr,
+ "fooABA=barABASW",
+ "Only the SameSite=None cookie set in ABA service worker is available."
+ );
+
+ await unregisterSW(ABABc);
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+});
+
+/**
+ * Verify a ABA service worker can only access partitioned cookies set in the
+ * ABA context if third-party cookies are blocked.
+ */
+add_task(async function testCookiesWithABAServiceWorker() {
+ // Disable blocking third-party cookies.
+ await SpecialPowers.pushPrefEnv({
+ set: [["network.cookie.cookieBehavior.optInPartitioning", true]],
+ });
+
+ info("Open a tab");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ TEST_TOP_PAGE
+ );
+
+ info("Load a ABA iframe");
+ let crossSiteBc = await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [TEST_CROSS_SITE_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ return ifr.browsingContext;
+ }
+ );
+
+ let ABABc = await SpecialPowers.spawn(
+ crossSiteBc,
+ [TEST_TOP_PAGE],
+ async url => {
+ let ifr = content.document.createElement("iframe");
+
+ await new content.Promise(resolve => {
+ ifr.onload = resolve;
+ content.document.body.appendChild(ifr);
+ ifr.src = url;
+ });
+
+ return ifr.browsingContext;
+ }
+ );
+
+ info("Set cookies in the ABA iframe");
+ await SpecialPowers.spawn(ABABc, [], async _ => {
+ const cookies = [
+ "fooABA=barABA; SameSite=None; Secure",
+ "fooABALax=BarABALax; SameSite=Lax; Secure",
+ "fooABAPartitioned=barABA; SameSite=None; Secure; Partitioned;",
+ "fooABALaxPartitioned=BarABALax; SameSite=Lax; Secure; Partitioned;",
+ ];
+
+ let query = "";
+
+ for (let cookie of cookies) {
+ query += `Set-Cookie=${cookie}&`;
+ }
+
+ await content.fetch(`cookies.sjs?${query}`);
+ });
+
+ info(
+ "Register a service worker and trigger a fetch request to get cookies in the ABA context."
+ );
+
+ await registerSW(ABABc);
+
+ info("Get cookies in the ABA service worker.");
+ let cookieStr = await fetchCookiesFromSW(ABABc);
+
+ is(
+ cookieStr,
+ "fooABAPartitioned=barABA",
+ "Only the SameSite=None partitioned cookie set in ABA iframe is available."
+ );
+
+ info("Set cookies from the service worker in ABA context");
+ await setCookiesFromSW(ABABc, [
+ "fooABA=barABASW; SameSite=None; Secure",
+ "fooABALax=BarABALaxSW; SameSite=Lax; Secure",
+ "fooABAPartitioned=barABASW; SameSite=None; Secure; Partitioned;",
+ "fooABALaxPartitioned=BarABALaxSW; SameSite=Lax; Secure; Partitioned;",
+ ]);
+
+ info("Get cookies from the service worker in the ABA context.");
+ cookieStr = await fetchCookiesFromSW(ABABc);
+
+ is(
+ cookieStr,
+ "fooABAPartitioned=barABASW",
+ "Only the SameSite=None partitioned cookie set in ABA service worker is available."
+ );
+
+ await unregisterSW(ABABc);
+ BrowserTestUtils.removeTab(tab);
+ Services.cookies.removeAll();
+});
diff --git a/netwerk/cookie/test/browser/chips.sjs b/netwerk/cookie/test/browser/chips.sjs
new file mode 100644
index 0000000000..e60109f6fa
--- /dev/null
+++ b/netwerk/cookie/test/browser/chips.sjs
@@ -0,0 +1,28 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+
+ var params = new URLSearchParams(aRequest.queryString);
+
+ // Get Cookie header string.
+ if (params.has("get")) {
+ if (aRequest.hasHeader("Cookie")) {
+ let cookie = aRequest.getHeader("Cookie");
+ aResponse.write(cookie);
+ }
+ return;
+ }
+
+ // Set a partitioned and a unpartitioned cookie.
+ if (params.has("set")) {
+ aResponse.setHeader(
+ "Set-Cookie",
+ "cookie=partitioned; Partitioned; SameSite=None; Secure",
+ true
+ );
+ aResponse.setHeader(
+ "Set-Cookie",
+ "cookie=unpartitioned; SameSite=None; Secure",
+ true
+ );
+ }
+}
diff --git a/netwerk/cookie/test/browser/cookies.sjs b/netwerk/cookie/test/browser/cookies.sjs
new file mode 100644
index 0000000000..9beb861d44
--- /dev/null
+++ b/netwerk/cookie/test/browser/cookies.sjs
@@ -0,0 +1,17 @@
+function handleRequest(aRequest, aResponse) {
+ aResponse.setStatusLine(aRequest.httpVersion, 200);
+ let query = new URLSearchParams(aRequest.queryString);
+
+ if (query.has("Set-Cookie")) {
+ for (let value of query.getAll("Set-Cookie")) {
+ aResponse.setHeader("Set-Cookie", value, true);
+ }
+ return;
+ }
+
+ let cookieStr = "";
+ if (aRequest.hasHeader("Cookie")) {
+ cookieStr = aRequest.getHeader("Cookie");
+ }
+ aResponse.write(cookieStr);
+}
diff --git a/netwerk/cookie/test/browser/serviceWorker.js b/netwerk/cookie/test/browser/serviceWorker.js
new file mode 100644
index 0000000000..b2eab40ce4
--- /dev/null
+++ b/netwerk/cookie/test/browser/serviceWorker.js
@@ -0,0 +1,21 @@
+self.addEventListener("install", function () {
+ self.skipWaiting();
+});
+
+self.addEventListener("activate", function (event) {
+ event.waitUntil(self.clients.claim());
+});
+
+self.addEventListener("message", function (event) {
+ if (event.data.action === "fetch") {
+ fetch(event.data.url)
+ .then(response => response.text())
+ .then(data => {
+ self.clients.matchAll().then(clients => {
+ clients.forEach(client => {
+ client.postMessage({ content: data });
+ });
+ });
+ });
+ }
+});