Bug 1713203 - Added check for host, scheme and origin attributes check before cookie change broadcast to content processes r=dveditz,dragana,nika

Differential Revision: https://phabricator.services.mozilla.com/D146346
This commit is contained in:
edguloien 2022-08-23 17:36:24 +00:00
parent 90e860f7bd
commit 6ff16b6810
22 changed files with 1123 additions and 19 deletions

View File

@ -162,6 +162,7 @@
#include "mozilla/net/NeckoMessageUtils.h"
#include "mozilla/net/NeckoParent.h"
#include "mozilla/net/PCookieServiceParent.h"
#include "mozilla/net/CookieKey.h"
#include "mozilla/TelemetryComms.h"
#include "mozilla/TelemetryEventEnums.h"
#include "mozilla/RemoteLazyInputStreamParent.h"
@ -3946,11 +3947,18 @@ ContentParent::Observe(nsISupports* aSubject, const char* aTopic,
nsCOMPtr<nsICookie> xpcCookie = do_QueryInterface(aSubject);
NS_ASSERTION(xpcCookie, "couldn't get cookie");
// only broadcast the cookie change to content processes that need it
const Cookie& cookie = xpcCookie->AsCookie();
if (!cs->CookieMatchesContentList(cookie)) {
return NS_OK;
}
if (!nsCRT::strcmp(aData, u"deleted")) {
cs->RemoveCookie(xpcCookie);
cs->RemoveCookie(cookie);
} else if ((!nsCRT::strcmp(aData, u"added")) ||
(!nsCRT::strcmp(aData, u"changed"))) {
cs->AddCookie(xpcCookie);
cs->AddCookie(cookie);
}
} else if (!strcmp(aTopic, NS_NETWORK_LINK_TYPE_TOPIC)) {
UpdateNetworkLinkType();

View File

@ -158,6 +158,8 @@ Cookie::GetOriginAttributes(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
return NS_OK;
}
const Cookie& Cookie::AsCookie() { return *this; }
const nsCString& Cookie::GetFilePath() {
MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());

View File

@ -191,6 +191,13 @@ IPCResult CookieServiceChild::RecvAddCookie(const CookieStruct& aCookie,
const OriginAttributes& aAttrs) {
RefPtr<Cookie> cookie = Cookie::Create(aCookie, aAttrs);
RecordDocumentCookie(cookie, aAttrs);
// signal test code to check their cookie list
nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
if (obsService) {
obsService->NotifyObservers(nullptr, "cookie-content-filter-test", nullptr);
}
return IPC_OK();
}

View File

@ -4,6 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "CookieCommons.h"
#include "CookieLogging.h"
#include "mozilla/net/CookieService.h"
#include "mozilla/net/CookieServiceParent.h"
#include "mozilla/net/NeckoParent.h"
@ -13,7 +14,9 @@
#include "mozIThirdPartyUtil.h"
#include "nsArrayUtils.h"
#include "nsIChannel.h"
#include "nsIEffectiveTLDService.h"
#include "nsNetCID.h"
#include "nsMixedContentBlocker.h"
using namespace mozilla::ipc;
@ -28,6 +31,10 @@ CookieServiceParent::CookieServiceParent() {
// Get the CookieService instance directly, so we can call internal methods.
mCookieService = CookieService::GetSingleton();
NS_ASSERTION(mCookieService, "couldn't get nsICookieService");
mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
MOZ_ALWAYS_TRUE(mTLDService);
mProcessingCookie = false;
}
@ -40,10 +47,10 @@ void CookieServiceParent::RemoveBatchDeletedCookies(nsIArray* aCookieList) {
nsTArray<OriginAttributes> attrsList;
for (uint32_t i = 0; i < len; i++) {
nsCOMPtr<nsICookie> xpcCookie = do_QueryElementAt(aCookieList, i);
auto* cookie = static_cast<Cookie*>(xpcCookie.get());
attrs = cookie->OriginAttributesRef();
cookieStruct = cookie->ToIPC();
if (cookie->IsHttpOnly()) {
const auto& cookie = xpcCookie->AsCookie();
attrs = cookie.OriginAttributesRef();
cookieStruct = cookie.ToIPC();
if (cookie.IsHttpOnly()) {
// Child only needs to exist if an HttpOnly cookie exists, not its value
cookieStruct.value() = "";
}
@ -55,26 +62,36 @@ void CookieServiceParent::RemoveBatchDeletedCookies(nsIArray* aCookieList) {
void CookieServiceParent::RemoveAll() { Unused << SendRemoveAll(); }
void CookieServiceParent::RemoveCookie(nsICookie* aCookie) {
auto* cookie = static_cast<Cookie*>(aCookie);
const OriginAttributes& attrs = cookie->OriginAttributesRef();
CookieStruct cookieStruct = cookie->ToIPC();
if (cookie->IsHttpOnly()) {
void CookieServiceParent::RemoveCookie(const Cookie& cookie) {
const OriginAttributes& attrs = cookie.OriginAttributesRef();
CookieStruct cookieStruct = cookie.ToIPC();
if (cookie.IsHttpOnly()) {
cookieStruct.value() = "";
}
Unused << SendRemoveCookie(cookieStruct, attrs);
}
void CookieServiceParent::AddCookie(nsICookie* aCookie) {
auto* cookie = static_cast<Cookie*>(aCookie);
const OriginAttributes& attrs = cookie->OriginAttributesRef();
CookieStruct cookieStruct = cookie->ToIPC();
if (cookie->IsHttpOnly()) {
void CookieServiceParent::AddCookie(const Cookie& cookie) {
const OriginAttributes& attrs = cookie.OriginAttributesRef();
CookieStruct cookieStruct = cookie.ToIPC();
if (cookie.IsHttpOnly()) {
cookieStruct.value() = "";
}
Unused << SendAddCookie(cookieStruct, attrs);
}
bool CookieServiceParent::CookieMatchesContentList(const Cookie& cookie) {
nsCString baseDomain;
MOZ_ALWAYS_SUCCEEDS(CookieCommons::GetBaseDomainFromHost(
mTLDService, cookie.Host(), baseDomain));
CookieKey cookieKey(baseDomain, cookie.OriginAttributesRef());
if (Maybe<bool> allowSecure = mCookieKeysInContent.MaybeGet(cookieKey)) {
return (!cookie.IsSecure() || *allowSecure);
}
return false;
}
void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
nsCOMPtr<nsIURI> uri;
aChannel->GetURI(getter_AddRefs(uri));
@ -89,7 +106,6 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
StoragePrincipalHelper::PrepareEffectiveStoragePrincipalOriginAttributes(
aChannel, attrs);
// Send matching cookies to Child.
nsCOMPtr<mozIThirdPartyUtil> thirdPartyUtil;
thirdPartyUtil = do_GetService(THIRDPARTYUTIL_CONTRACTID);
@ -97,6 +113,9 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
ThirdPartyAnalysisResult result = thirdPartyUtil->AnalyzeChannel(
aChannel, false, nullptr, nullptr, &rejectedReason);
UpdateCookieInContentList(uri, attrs);
// Send matching cookies to Child.
nsTArray<Cookie*> foundCookieList;
mCookieService->GetCookiesForURI(
uri, aChannel, result.contains(ThirdPartyAnalysis::IsForeign),
@ -110,6 +129,21 @@ void CookieServiceParent::TrackCookieLoad(nsIChannel* aChannel) {
Unused << SendTrackCookiesLoad(matchingCookiesList, attrs);
}
// we append outgoing cookie info into a list here so the ContentParent can
// filter cookies passing to unnecessary ContentProcesses
void CookieServiceParent::UpdateCookieInContentList(
nsIURI* uri, const OriginAttributes& originAttrs) {
nsCString baseDomain;
bool requireAHostMatch = false;
MOZ_ALWAYS_SUCCEEDS(CookieCommons::GetBaseDomain(mTLDService, uri, baseDomain,
requireAHostMatch));
CookieKey cookieKey(baseDomain, originAttrs);
bool& allowSecure = mCookieKeysInContent.LookupOrInsert(cookieKey, false);
allowSecure =
allowSecure || nsMixedContentBlocker::IsPotentiallyTrustworthyOrigin(uri);
}
// static
void CookieServiceParent::SerialializeCookieList(
const nsTArray<Cookie*>& aFoundCookieList,
@ -138,6 +172,10 @@ IPCResult CookieServiceParent::RecvPrepareCookieList(
return IPC_FAIL(this, "aHost must not be null");
}
// we append outgoing cookie info into a list here so the ContentParent can
// filter cookies that do not need to go to certain ContentProcesses
UpdateCookieInContentList(aHost, aAttrs);
nsTArray<Cookie*> foundCookieList;
// Note: passing nullptr as aChannel to GetCookiesForURI() here is fine since
// this argument is only used for proper reporting of cookie loads, but the

View File

@ -7,6 +7,7 @@
#define mozilla_net_CookieServiceParent_h
#include "mozilla/net/PCookieServiceParent.h"
#include "mozilla/net/CookieKey.h"
class nsIArray;
class nsICookie;
@ -14,6 +15,8 @@ namespace mozilla {
class OriginAttributes;
}
class nsIEffectiveTLDService;
namespace mozilla {
namespace net {
@ -33,9 +36,9 @@ class CookieServiceParent : public PCookieServiceParent {
void RemoveAll();
void RemoveCookie(nsICookie* aCookie);
void RemoveCookie(const Cookie& aCookie);
void AddCookie(nsICookie* aCookie);
void AddCookie(const Cookie& aCookie);
// This will return true if the CookieServiceParent is currently processing
// an update from the content process. This is used in ContentParent to make
@ -43,6 +46,10 @@ class CookieServiceParent : public PCookieServiceParent {
// processes, not the one they originated from.
bool ProcessingCookie() { return mProcessingCookie; }
bool CookieMatchesContentList(const Cookie& cookie);
void UpdateCookieInContentList(nsIURI* aHostURI,
const OriginAttributes& aOriginAttrs);
protected:
virtual void ActorDestroy(ActorDestroyReason aWhy) override;
@ -62,8 +69,10 @@ class CookieServiceParent : public PCookieServiceParent {
static void SerialializeCookieList(const nsTArray<Cookie*>& aFoundCookieList,
nsTArray<CookieStruct>& aCookiesList);
nsCOMPtr<nsIEffectiveTLDService> mTLDService;
RefPtr<CookieService> mCookieService;
bool mProcessingCookie;
nsTHashMap<CookieKey, bool> mCookieKeysInContent;
};
} // namespace net

View File

@ -10,6 +10,14 @@
* Main cookie object interface.
*/
%{C++
namespace mozilla::net {
class Cookie;
}
%}
[ref] native const_Cookie(const mozilla::net::Cookie);
typedef long nsCookieStatus;
typedef long nsCookiePolicy;
@ -80,6 +88,9 @@ interface nsICookie : nsISupports {
[implicit_jscontext]
readonly attribute jsval originAttributes;
[noscript, notxpcom, nostdcall, binaryname(AsCookie)]
const_Cookie AsCookie();
/**
* true if the cookie is a session cookie.
* note that expiry time will also be honored

View File

@ -55,6 +55,13 @@ support-files =
103_preload_csp_imgsrc_none.html
103_preload_csp_imgsrc_none.html^headers^
103_preload_csp_imgsrc_none.html^informationalResponse^
cookie_filtering_resource.sjs
cookie_filtering_secure_resource_com.html
cookie_filtering_secure_resource_com.html^headers^
cookie_filtering_secure_resource_org.html
cookie_filtering_secure_resource_org.html^headers^
cookie_filtering_square.png
cookie_filtering_square.png^headers^
[browser_about_cache.js]
[browser_bug1535877.js]
@ -112,6 +119,11 @@ skip-if =
[browser_103_assets.js]
skip-if =
os == 'linux' && bits == 64 && !debug # Bug 1744028 and Bug 1746324
[browser_cookie_filtering_basic.js]
[browser_cookie_filtering_insecure.js]
[browser_cookie_filtering_oa.js]
[browser_cookie_filtering_cross_origin.js]
[browser_cookie_filtering_subdomain.js]
[browser_103_user_load.js]
support-files =
early_hint_preload_test_helper.jsm

View File

@ -101,3 +101,7 @@ add_task(async function() {
gBrowser.removeCurrentTab();
});
add_task(async function cleanup() {
Services.prefs.clearUserPref("dom.securecontext.allowlist");
});

View File

@ -0,0 +1,182 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const {
HTTPS_EXAMPLE_ORG,
HTTPS_EXAMPLE_COM,
HTTP_EXAMPLE_COM,
browserTestPath,
waitForAllExpectedTests,
cleanupObservers,
checkExpectedCookies,
fetchHelper,
preclean_test,
cleanup_test,
} = ChromeUtils.import("resource://testing-common/cookie_filtering_helper.jsm");
// run suite with content listener
// 1. initializes the content process and observer
// 2. runs the test gamut
// 3. cleans up the content process
async function runSuiteWithContentListener(name, triggerSuiteFunc, expected) {
return async function(browser) {
info("Running content suite: " + name);
await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies);
await triggerSuiteFunc();
await SpecialPowers.spawn(browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(browser, [], cleanupObservers);
info("Complete content suite: " + name);
};
}
// TEST: Different domains (org)
// * example.org cookies go to example.org process
// * exmaple.com cookies do not go to example.org process
async function test_basic_suite_org() {
// example.org - start content process when loading page
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_EXAMPLE_ORG),
},
await runSuiteWithContentListener(
"basic suite org",
triggerBasicSuite,
basicSuiteMatchingDomain(HTTPS_EXAMPLE_ORG)
)
);
}
// TEST: Different domains (com)
// * example.com cookies go to example.com process
// * example.org cookies do not go to example.com process
// * insecure example.com cookies go to secure com process
async function test_basic_suite_com() {
// example.com - start content process when loading page
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"basic suite com",
triggerBasicSuite,
basicSuiteMatchingDomain(HTTPS_EXAMPLE_COM).concat(
basicSuiteMatchingDomain(HTTP_EXAMPLE_COM)
)
)
);
}
// TEST: Duplicate domain (org)
// * example.org cookies go to multiple example.org processes
async function test_basic_suite_org_duplicate() {
let expected = basicSuiteMatchingDomain(HTTPS_EXAMPLE_ORG);
let t1 = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
browserTestPath(HTTPS_EXAMPLE_ORG)
);
let testStruct1 = {
name: "example.org primary",
browser: gBrowser.getBrowserForTab(t1),
tab: t1,
expected,
};
// example.org dup
let t3 = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
browserTestPath(HTTPS_EXAMPLE_ORG)
);
let testStruct3 = {
name: "example.org dup",
browser: gBrowser.getBrowserForTab(t3),
tab: t3,
expected,
};
let parentpid = Services.appinfo.processID;
let pid1 = testStruct1.browser.frameLoader.remoteTab.osPid;
let pid3 = testStruct3.browser.frameLoader.remoteTab.osPid;
ok(
parentpid != pid1,
"Parent pid should differ from content process for 1st example.org"
);
ok(
parentpid != pid3,
"Parent pid should differ from content process for 2nd example.org"
);
ok(pid1 != pid3, "Content pids should differ from each other");
await SpecialPowers.spawn(
testStruct1.browser,
[testStruct1.expected, testStruct1.name],
checkExpectedCookies
);
await SpecialPowers.spawn(
testStruct3.browser,
[testStruct3.expected, testStruct3.name],
checkExpectedCookies
);
await triggerBasicSuite();
// wait for all tests and cleanup
await SpecialPowers.spawn(testStruct1.browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(testStruct3.browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(testStruct1.browser, [], cleanupObservers);
await SpecialPowers.spawn(testStruct3.browser, [], cleanupObservers);
BrowserTestUtils.removeTab(testStruct1.tab);
BrowserTestUtils.removeTab(testStruct3.tab);
}
function basicSuite() {
var suite = [];
suite.push(["test-cookie=aaa", HTTPS_EXAMPLE_ORG]);
suite.push(["test-cookie=bbb", HTTPS_EXAMPLE_ORG]);
suite.push(["test-cookie=dad", HTTPS_EXAMPLE_ORG]);
suite.push(["test-cookie=rad", HTTPS_EXAMPLE_ORG]);
suite.push(["test-cookie=orgwontsee", HTTPS_EXAMPLE_COM]);
suite.push(["test-cookie=sentinelorg", HTTPS_EXAMPLE_ORG]);
suite.push(["test-cookie=sentinelcom", HTTPS_EXAMPLE_COM]);
return suite;
}
function basicSuiteMatchingDomain(domain) {
var suite = basicSuite();
var result = [];
for (var [cookie, dom] of suite) {
if (dom == domain) {
result.push(cookie);
}
}
return result;
}
// triggers set-cookie, which will trigger cookie-changed messages
// messages will be filtered against the cookie list created from above
// only unfiltered messages should make it to the content process
async function triggerBasicSuite() {
let triggerCookies = basicSuite();
for (var [cookie, domain] of triggerCookies) {
let secure = false;
if (domain.includes("https")) {
secure = true;
}
//trigger
var url = browserTestPath(domain) + "cookie_filtering_resource.sjs";
await fetchHelper(url, cookie, secure);
}
}
add_task(preclean_test);
add_task(test_basic_suite_org); // 5
add_task(test_basic_suite_com); // 2
add_task(test_basic_suite_org_duplicate); // 13
add_task(cleanup_test);

View File

@ -0,0 +1,144 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const {
HTTPS_EXAMPLE_ORG,
HTTPS_EXAMPLE_COM,
HTTP_EXAMPLE_COM,
browserTestPath,
waitForAllExpectedTests,
cleanupObservers,
checkExpectedCookies,
preclean_test,
cleanup_test,
} = ChromeUtils.import("resource://testing-common/cookie_filtering_helper.jsm");
async function runSuiteWithContentListener(name, trigger_suite_func, expected) {
return async function(browser) {
info("Running content suite: " + name);
await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies);
await trigger_suite_func();
await SpecialPowers.spawn(browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(browser, [], cleanupObservers);
info("Complete content suite: " + name);
};
}
// TEST: Cross Origin Resource (com)
// * process receives only COR cookies pertaining to same page
async function test_cross_origin_resource_com() {
let comExpected = [];
comExpected.push("test-cookie=comhtml"); // 1
comExpected.push("test-cookie=png"); // 2
comExpected.push("test-cookie=orghtml"); // 3
// nothing for 4, 5, 6, 7 -> goes to .org process
comExpected.push("test-cookie=png"); // 8
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"COR example.com",
triggerCrossOriginSuite,
comExpected
)
);
Services.cookies.removeAll();
}
// TEST: Cross Origin Resource (org)
// * received COR cookies only pertaining to the process's page
async function test_cross_origin_resource_org() {
let orgExpected = [];
// nothing for 1, 2 and 3 -> goes to .com
orgExpected.push("test-cookie=png"); // 4
orgExpected.push("test-cookie=orghtml"); // 5
orgExpected.push("test-cookie=png"); // 6
orgExpected.push("test-cookie=comhtml"); // 7
// 8 nothing for 8 -> goes to .com process
orgExpected.push("test-cookie=png"); // 9. Sentinel to verify no more came in
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_EXAMPLE_ORG),
},
await runSuiteWithContentListener(
"COR example.org",
triggerCrossOriginSuite,
orgExpected
)
);
}
// seems to block better than fetch for secondary resource
// using for more predicatable recving
async function requestBrowserPageWithFilename(
testName,
requestFrom,
filename,
param = ""
) {
let url = requestFrom + "/browser/netwerk/test/browser/" + filename;
// add param if necessary
if (param != "") {
url += "?" + param;
}
info("requesting " + url + " (" + testName + ")");
await BrowserTestUtils.withNewTab(
{
gBrowser,
url,
},
async () => {}
);
}
async function triggerCrossOriginSuite() {
// SameSite - 1 com page, 2 com png
await requestBrowserPageWithFilename(
"SameSite resource (com)",
HTTPS_EXAMPLE_COM,
"cookie_filtering_secure_resource_com.html"
);
// COR - 3 com page, 4 org png
await requestBrowserPageWithFilename(
"COR (com-org)",
HTTPS_EXAMPLE_COM,
"cookie_filtering_secure_resource_org.html"
);
// SameSite - 5 org page, 6 org png
await requestBrowserPageWithFilename(
"SameSite resource (org)",
HTTPS_EXAMPLE_ORG,
"cookie_filtering_secure_resource_org.html"
);
// COR - 7 org page, 8 com png
await requestBrowserPageWithFilename(
"SameSite resource (org-com)",
HTTPS_EXAMPLE_ORG,
"cookie_filtering_secure_resource_com.html"
);
// Sentinel to verify no more cookies come in after last true test
await requestBrowserPageWithFilename(
"COR sentinel",
HTTPS_EXAMPLE_ORG,
"cookie_filtering_square.png"
);
}
add_task(preclean_test);
add_task(test_cross_origin_resource_com); // 4
add_task(test_cross_origin_resource_org); // 5
add_task(cleanup_test);

View File

@ -0,0 +1,103 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const {
HTTPS_EXAMPLE_ORG,
HTTPS_EXAMPLE_COM,
HTTP_EXAMPLE_COM,
browserTestPath,
waitForAllExpectedTests,
cleanupObservers,
checkExpectedCookies,
fetchHelper,
preclean_test,
cleanup_test,
} = ChromeUtils.import("resource://testing-common/cookie_filtering_helper.jsm");
async function runSuiteWithContentListener(name, trigger_suite_func, expected) {
return async function(browser) {
info("Running content suite: " + name);
await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies);
await trigger_suite_func();
await SpecialPowers.spawn(browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(browser, [], cleanupObservers);
info("Complete content suite: " + name);
};
}
// TEST: In/Secure (insecure com)
// * secure example.com cookie do not go to insecure example.com process
// * insecure cookies go to insecure process
// * secure request with insecure cookie will go to insecure process
async function test_insecure_suite_insecure_com() {
var expected = [];
expected.push("test-cookie=png1");
expected.push("test-cookie=png2");
// insecure com will not recieve the secure com request with secure cookie
expected.push("test-cookie=png3");
info(expected);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTP_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"insecure suite insecure com",
triggerInsecureSuite,
expected
)
);
}
// TEST: In/Secure (secure com)
// * secure page will recieve all secure/insecure cookies
async function test_insecure_suite_secure_com() {
var expected = [];
expected.push("test-cookie=png1");
expected.push("test-cookie=png2");
expected.push("test-cookie=secure-png");
expected.push("test-cookie=png3");
info(expected);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"insecure suite secure com",
triggerInsecureSuite,
expected
)
);
}
async function triggerInsecureSuite() {
const cookieSjsFilename = "cookie_filtering_resource.sjs";
// insecure page, insecure cookie
var url = browserTestPath(HTTP_EXAMPLE_COM) + cookieSjsFilename;
await fetchHelper(url, "test-cookie=png1", false);
// secure page req, insecure cookie
url = browserTestPath(HTTPS_EXAMPLE_COM) + cookieSjsFilename;
await fetchHelper(url, "test-cookie=png2", false);
// secure page, secure cookie
url = browserTestPath(HTTPS_EXAMPLE_COM) + cookieSjsFilename;
await fetchHelper(url, "test-cookie=secure-png", true);
// not testing insecure server returning secure cookie --
// sentinel
url = browserTestPath(HTTPS_EXAMPLE_COM) + cookieSjsFilename;
await fetchHelper(url, "test-cookie=png3", false);
}
add_task(preclean_test);
add_task(test_insecure_suite_insecure_com); // 3
add_task(test_insecure_suite_secure_com); // 4
add_task(cleanup_test);

View File

@ -0,0 +1,187 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const {
HTTPS_EXAMPLE_ORG,
HTTPS_EXAMPLE_COM,
HTTP_EXAMPLE_COM,
browserTestPath,
waitForAllExpectedTests,
cleanupObservers,
checkExpectedCookies,
triggerSetCookieFromHttp,
triggerSetCookieFromHttpPrivate,
preclean_test,
cleanup_test,
} = ChromeUtils.import("resource://testing-common/cookie_filtering_helper.jsm");
// TEST: OriginAttributes
// * example.com OA-changed cookies don't go to example.com & vice-versa
async function test_origin_attributes() {
var suite = oaSuite();
// example.com - start content process when loading page
let t2 = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
browserTestPath(HTTPS_EXAMPLE_COM)
);
let testStruct2 = {
name: "OA example.com",
browser: gBrowser.getBrowserForTab(t2),
tab: t2,
expected: [suite[0], suite[4]],
};
// open a tab with altered OA: userContextId
let t4 = await BrowserTestUtils.openNewForegroundTab({
gBrowser,
opening: (function() {
return function() {
// info("calling addTab from lambda");
gBrowser.selectedTab = BrowserTestUtils.addTab(
gBrowser,
HTTPS_EXAMPLE_COM,
{ userContextId: 1 }
);
};
})(),
});
let testStruct4 = {
name: "OA example.com (contextId)",
browser: gBrowser.getBrowserForTab(t4),
tab: t4,
expected: [suite[2], suite[5]],
};
// example.com
await SpecialPowers.spawn(
testStruct2.browser,
[testStruct2.expected, testStruct2.name],
checkExpectedCookies
);
// example.com with different OA: userContextId
await SpecialPowers.spawn(
testStruct4.browser,
[testStruct4.expected, testStruct4.name],
checkExpectedCookies
);
await triggerOriginAttributesEmulatedSuite();
await SpecialPowers.spawn(testStruct2.browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(testStruct4.browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(testStruct2.browser, [], cleanupObservers);
await SpecialPowers.spawn(testStruct4.browser, [], cleanupObservers);
BrowserTestUtils.removeTab(testStruct2.tab);
BrowserTestUtils.removeTab(testStruct4.tab);
}
// TEST: Private
// * example.com private cookies don't go to example.com process & vice-v
async function test_private() {
var suite = oaSuite();
// example.com
let t2 = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
browserTestPath(HTTPS_EXAMPLE_COM)
);
let testStruct2 = {
name: "non-priv example.com",
browser: gBrowser.getBrowserForTab(t2),
tab: t2,
expected: [suite[0], suite[4]],
};
// private window example.com
let privateBrowserWindow = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
let privateTab = (privateBrowserWindow.gBrowser.selectedTab = BrowserTestUtils.addTab(
privateBrowserWindow.gBrowser,
browserTestPath(HTTPS_EXAMPLE_COM)
));
let testStruct5 = {
name: "private example.com",
browser: privateBrowserWindow.gBrowser.getBrowserForTab(privateTab),
tab: privateTab,
expected: [suite[3], suite[6]],
};
await BrowserTestUtils.browserLoaded(testStruct5.tab.linkedBrowser);
let parentpid = Services.appinfo.processID;
let privatePid = testStruct5.browser.frameLoader.remoteTab.osPid;
let pid = testStruct2.browser.frameLoader.remoteTab.osPid;
ok(parentpid != privatePid, "Parent and private processes are unique");
ok(parentpid != pid, "Parent and non-private processes are unique");
ok(privatePid != pid, "Private and non-private processes are unique");
// example.com
await SpecialPowers.spawn(
testStruct2.browser,
[testStruct2.expected, testStruct2.name],
checkExpectedCookies
);
// example.com private
await SpecialPowers.spawn(
testStruct5.browser,
[testStruct5.expected, testStruct5.name],
checkExpectedCookies
);
await triggerOriginAttributesEmulatedSuite();
// wait for all tests and cleanup
await SpecialPowers.spawn(testStruct2.browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(testStruct5.browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(testStruct2.browser, [], cleanupObservers);
await SpecialPowers.spawn(testStruct5.browser, [], cleanupObservers);
BrowserTestUtils.removeTab(testStruct2.tab);
BrowserTestUtils.removeTab(testStruct5.tab);
await BrowserTestUtils.closeWindow(privateBrowserWindow);
}
function oaSuite() {
var suite = [];
suite.push("test-cookie=orgwontsee"); // 0
suite.push("test-cookie=firstparty"); // 1
suite.push("test-cookie=usercontext"); // 2
suite.push("test-cookie=privateonly"); // 3
suite.push("test-cookie=sentinelcom"); // 4
suite.push("test-cookie=sentineloa"); // 5
suite.push("test-cookie=sentinelprivate"); // 6
return suite;
}
// emulated because we are not making actual page requests
// we are just directly invoking the api
async function triggerOriginAttributesEmulatedSuite() {
var suite = oaSuite();
let uriCom = NetUtil.newURI(HTTPS_EXAMPLE_COM);
triggerSetCookieFromHttp(uriCom, suite[0]);
// FPD (OA) changed
triggerSetCookieFromHttp(uriCom, suite[1], "foo.com");
// context id (OA) changed
triggerSetCookieFromHttp(uriCom, suite[2], "", 1);
// private
triggerSetCookieFromHttpPrivate(uriCom, suite[3]);
// sentinels
triggerSetCookieFromHttp(uriCom, suite[4]);
triggerSetCookieFromHttp(uriCom, suite[5], "", 1);
triggerSetCookieFromHttpPrivate(uriCom, suite[6]);
}
add_task(preclean_test);
add_task(test_origin_attributes); // 4
add_task(test_private); // 7
add_task(cleanup_test);

View File

@ -0,0 +1,167 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const {
HTTPS_EXAMPLE_ORG,
HTTPS_EXAMPLE_COM,
HTTP_EXAMPLE_COM,
browserTestPath,
waitForAllExpectedTests,
cleanupObservers,
checkExpectedCookies,
fetchHelper,
preclean_test,
cleanup_test,
} = ChromeUtils.import("resource://testing-common/cookie_filtering_helper.jsm");
const HTTPS_SUBDOMAIN_1_EXAMPLE_COM = "https://test1.example.com";
const HTTP_SUBDOMAIN_1_EXAMPLE_COM = "http://test1.example.com";
const HTTPS_SUBDOMAIN_2_EXAMPLE_COM = "https://test2.example.com";
const HTTP_SUBDOMAIN_2_EXAMPLE_COM = "http://test2.example.com";
// run suite with content listener
// 1. initializes the content process and observer
// 2. runs the test gamut
// 3. cleans up the content process
async function runSuiteWithContentListener(name, triggerSuiteFunc, expected) {
return async function(browser) {
info("Running content suite: " + name);
await SpecialPowers.spawn(browser, [expected, name], checkExpectedCookies);
await triggerSuiteFunc();
await SpecialPowers.spawn(browser, [], waitForAllExpectedTests);
await SpecialPowers.spawn(browser, [], cleanupObservers);
info("Complete content suite: " + name);
};
}
// TEST: domain receives subdomain cookies
async function test_domain() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"test_domain",
triggerSuite,
cookiesFromSuite()
)
);
}
// TEST: insecure domain receives base and sub-domain insecure cookies
async function test_insecure_domain() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTP_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"test_insecure_domain",
triggerSuite,
suiteMatchingDomain(HTTP_EXAMPLE_COM).concat(
suiteMatchingDomain(HTTP_SUBDOMAIN_1_EXAMPLE_COM)
)
)
);
}
// TEST: subdomain receives base domain and other sub-domain cookies
async function test_subdomain() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTPS_SUBDOMAIN_2_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"test_subdomain",
triggerSuite,
cookiesFromSuite()
)
);
}
// TEST: insecure subdomain receives base and sub-domain insecure cookies
async function test_insecure_subdomain() {
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: browserTestPath(HTTP_SUBDOMAIN_2_EXAMPLE_COM),
},
await runSuiteWithContentListener(
"test_insecure_domain",
triggerSuite,
suiteMatchingDomain(HTTP_EXAMPLE_COM).concat(
suiteMatchingDomain(HTTP_SUBDOMAIN_1_EXAMPLE_COM)
)
)
);
}
function suite() {
var suite = [];
suite.push(["test-cookie=domain", HTTPS_EXAMPLE_COM]);
suite.push(["test-cookie=subdomain", HTTPS_SUBDOMAIN_1_EXAMPLE_COM]);
suite.push(["test-cookie-insecure=insecure_domain", HTTP_EXAMPLE_COM]);
suite.push([
"test-cookie-insecure=insecure_subdomain",
HTTP_SUBDOMAIN_1_EXAMPLE_COM,
]);
suite.push(["test-cookie=sentinel", HTTPS_EXAMPLE_COM]);
return suite;
}
function cookiesFromSuite() {
var cookies = [];
for (var [cookie] of suite()) {
cookies.push(cookie);
}
return cookies;
}
function suiteMatchingDomain(domain) {
var s = suite();
var result = [];
for (var [cookie, dom] of s) {
if (dom == domain) {
result.push(cookie);
}
}
return result;
}
function justSitename(maybeSchemefulMaybeSubdomainSite) {
let mssArray = maybeSchemefulMaybeSubdomainSite.split("://");
let maybesubdomain = mssArray[mssArray.length - 1];
let msdArray = maybesubdomain.split(".");
return msdArray.slice(msdArray.length - 2, msdArray.length).join(".");
}
// triggers set-cookie, which will trigger cookie-changed messages
// messages will be filtered against the cookie list created from above
// only unfiltered messages should make it to the content process
async function triggerSuite() {
let triggerCookies = suite();
for (var [cookie, schemefulDomain] of triggerCookies) {
let secure = false;
if (schemefulDomain.includes("https")) {
secure = true;
}
var url =
browserTestPath(schemefulDomain) + "cookie_filtering_resource.sjs";
await fetchHelper(url, cookie, secure, justSitename(schemefulDomain));
Services.cookies.removeAll(); // clean cookies across secure/insecure runs
}
}
add_task(preclean_test);
add_task(test_domain); // 5
add_task(test_insecure_domain); // 2
add_task(test_subdomain); // 5
add_task(test_insecure_subdomain); // 2
add_task(cleanup_test);

View File

@ -0,0 +1,176 @@
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
const info = console.log;
var EXPORTED_SYMBOLS = [
"HTTPS_EXAMPLE_ORG",
"HTTPS_EXAMPLE_COM",
"HTTP_EXAMPLE_COM",
"browserTestPath",
"waitForAllExpectedTests",
"cleanupObservers",
"triggerSetCookieFromHttp",
"triggerSetCookieFromHttpPrivate",
"checkExpectedCookies",
"fetchHelper",
"preclean_test",
"cleanup_test",
];
var HTTPS_EXAMPLE_ORG = "https://example.org";
var HTTPS_EXAMPLE_COM = "https://example.com";
var HTTP_EXAMPLE_COM = "http://example.com";
function browserTestPath(uri) {
return uri + "/browser/netwerk/test/browser/";
}
function waitForAllExpectedTests() {
return ContentTaskUtils.waitForCondition(() => {
return content.testDone === true;
});
}
function cleanupObservers() {
Services.obs.notifyObservers(null, "cookie-content-filter-cleanup");
}
async function preclean_test() {
// enable all cookies for the set-cookie trigger via setCookieStringFromHttp
Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
Services.prefs.setBoolPref(
"network.cookieJarSettings.unblocked_for_testing",
true
);
Services.prefs.setBoolPref("network.cookie.sameSite.laxByDefault", false);
Services.prefs.setBoolPref(
"network.cookie.sameSite.noneRequiresSecure",
false
);
Services.prefs.setBoolPref("network.cookie.sameSite.schemeful", false);
Services.cookies.removeAll();
}
async function cleanup_test() {
Services.prefs.clearUserPref("network.cookie.cookieBehavior");
Services.prefs.clearUserPref(
"network.cookieJarSettings.unblocked_for_testing"
);
Services.prefs.clearUserPref("network.cookie.sameSite.laxByDefault");
Services.prefs.clearUserPref("network.cookie.sameSite.noneRequiresSecure");
Services.prefs.clearUserPref("network.cookie.sameSite.schemeful");
Services.cookies.removeAll();
}
async function fetchHelper(url, cookie, secure, domain = "") {
let headers = new Headers();
headers.append("return-set-cookie", cookie);
if (!secure) {
headers.append("return-insecure-cookie", cookie);
}
if (domain != "") {
headers.append("return-cookie-domain", domain);
}
info("fetching " + url);
await fetch(url, { headers });
}
// cookie header strings with multiple name=value pairs delimited by \n
// will trigger multiple "cookie-changed" signals
function triggerSetCookieFromHttp(uri, cookie, fpd = "", ucd = 0) {
info("about to trigger set-cookie: " + uri + " " + cookie);
let channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
});
if (fpd != "") {
channel.loadInfo.originAttributes = { firstPartyDomain: fpd };
}
if (ucd != 0) {
channel.loadInfo.originAttributes = { userContextId: ucd };
}
Services.cookies.setCookieStringFromHttp(uri, cookie, channel);
}
async function triggerSetCookieFromHttpPrivate(uri, cookie) {
info("about to trigger set-cookie: " + uri + " " + cookie);
let channel = NetUtil.newChannel({
uri,
loadUsingSystemPrincipal: true,
contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
}).QueryInterface(Ci.nsIPrivateBrowsingChannel);
channel.loadInfo.originAttributes = { privateBrowsingId: 1 };
channel.setPrivate(true);
Services.cookies.setCookieStringFromHttp(uri, cookie, channel);
}
// observer/listener function that will be run on the content processes
// listens and checks for the expected cookies
function checkExpectedCookies(expected, browserName) {
const COOKIE_FILTER_TEST_MESSAGE = "cookie-content-filter-test";
const COOKIE_FILTER_TEST_CLEANUP = "cookie-content-filter-cleanup";
// Counting the expected number of tests is vital to the integrity of these
// tests due to the fact that this test suite relies on triggering tests
// to occur on multiple content processes.
// As such, test modifications/bugs often lead to silent failures.
// Hence, we count to ensure we didn't break anything
// To reduce risk here, we modularize each test as much as possible to
// increase liklihood that a silent failure will trigger a no-test
// error/warning
content.testDone = false;
let testNumber = 0;
// setup observer that continues listening/testing
function obs(subject, topic) {
// cleanup trigger recieved -> tear down the observer
if (topic == COOKIE_FILTER_TEST_CLEANUP) {
info("cleaning up: " + browserName);
Services.obs.removeObserver(obs, COOKIE_FILTER_TEST_MESSAGE);
Services.obs.removeObserver(obs, COOKIE_FILTER_TEST_CLEANUP);
return;
}
// test trigger recv'd -> perform test on cookie contents
if (topic == COOKIE_FILTER_TEST_MESSAGE) {
info("Checking if cookie visible: " + browserName);
let result = content.document.cookie;
let resultStr =
"Result " +
result +
" == expected: " +
expected[testNumber] +
" in " +
browserName;
ok(result == expected[testNumber], resultStr);
testNumber++;
if (testNumber >= expected.length) {
info("finishing browser tests: " + browserName);
content.testDone = true;
}
return;
}
ok(false, "Didn't handle cookie message properly"); //
}
info("setting up observers: " + browserName);
Services.obs.addObserver(obs, COOKIE_FILTER_TEST_MESSAGE);
Services.obs.addObserver(obs, COOKIE_FILTER_TEST_CLEANUP);
}

View File

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
function handleRequest(request, response) {
response.setStatusLine(request.httpVersion, 200, "OK");
response.setHeader("Cache-Control", "no-cache", false);
response.setHeader("Content-Type", "text/html", false);
// configure set-cookie domain
let domain = "";
if (request.hasHeader("return-cookie-domain")) {
domain = "; Domain=" + request.getHeader("return-cookie-domain");
}
// configure set-cookie sameSite
let authStr = "; Secure";
if (request.hasHeader("return-insecure-cookie")) {
authStr = "";
}
// use headers to decide if we have them
if (request.hasHeader("return-set-cookie")) {
response.setHeader(
"Set-Cookie",
request.getHeader("return-set-cookie") + authStr + domain,
false
);
}
let body = "<!DOCTYPE html> <html> <body> true </body> </html>";
response.write(body);
}

View File

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<img src="https://example.com/browser/netwerk/test/browser/cookie_filtering_square.png" width="100px">
</body>
</html>

View File

@ -0,0 +1,2 @@
Cache-Control: no-store
Set-Cookie: test-cookie=comhtml

View File

@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<body>
<img src="https://example.org/browser/netwerk/test/browser/cookie_filtering_square.png" width="100px">
</body>
</html>

View File

@ -0,0 +1,2 @@
Cache-Control: no-store
Set-Cookie: test-cookie=orghtml

View File

@ -0,0 +1,2 @@
Cache-Control: no-cache
Set-Cookie: test-cookie=png

View File

@ -15,6 +15,7 @@ XPCSHELL_TESTS_MANIFESTS += [
]
TESTING_JS_MODULES += [
"browser/cookie_filtering_helper.jsm",
"browser/early_hint_preload_test_helper.jsm",
"unit/test_http3_prio_helpers.js",
]