diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp index b34ad3970c1e..3c074b091548 100644 --- a/dom/base/Document.cpp +++ b/dom/base/Document.cpp @@ -3998,7 +3998,8 @@ nsresult Document::InitCOEP(nsIChannel* aChannel) { nsILoadInfo::CrossOriginEmbedderPolicy policy = nsILoadInfo::EMBEDDER_POLICY_NULL; - if (NS_SUCCEEDED(intChannel->GetResponseEmbedderPolicy(&policy))) { + if (NS_SUCCEEDED(intChannel->GetResponseEmbedderPolicy( + mTrials.IsEnabled(OriginTrial::CoepCredentialless), &policy))) { mEmbedderPolicy = Some(policy); } @@ -6874,6 +6875,15 @@ void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) { if (aHeaderField == nsGkAtoms::origin_trial) { mTrials.UpdateFromToken(aData, NodePrincipal()); + if (mTrials.IsEnabled(OriginTrial::CoepCredentialless)) { + InitCOEP(mChannel); + + WindowContext* ctx = GetWindowContext(); + MOZ_ASSERT(ctx); + if (mEmbedderPolicy) { + Unused << ctx->SetEmbedderPolicy(mEmbedderPolicy.value()); + } + } } if (aHeaderField == nsGkAtoms::headerDefaultStyle) { diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp index 47e6aa9724fd..a9201a271a9a 100644 --- a/dom/base/nsDOMWindowUtils.cpp +++ b/dom/base/nsDOMWindowUtils.cpp @@ -4711,6 +4711,18 @@ nsDOMWindowUtils::IsCssPropertyRecordedInUseCounter(const nsACString& aPropName, return knownProp ? NS_OK : NS_ERROR_FAILURE; } +NS_IMETHODIMP +nsDOMWindowUtils::IsCoepCredentialless(bool* aResult) { + Document* doc = GetDocument(); + if (!doc) { + return NS_ERROR_FAILURE; + } + + *aResult = IsCoepCredentiallessEnabled( + doc->Trials().IsEnabled(OriginTrial::CoepCredentialless)); + return NS_OK; +} + NS_IMETHODIMP nsDOMWindowUtils::GetLayersId(uint64_t* aOutLayersId) { nsIWidget* widget = GetWidget(); diff --git a/dom/fetch/tests/browser.ini b/dom/fetch/tests/browser.ini index 8705dea1ab1c..6fd8244288aa 100644 --- a/dom/fetch/tests/browser.ini +++ b/dom/fetch/tests/browser.ini @@ -1,2 +1,23 @@ [DEFAULT] [browser_blobFromFile.js] +[browser_origin_trial_coep_credentialless_fetch_1.js] +support-files = + open_credentialless_document.sjs + store_header.sjs +[browser_origin_trial_coep_credentialless_fetch_2.js] +support-files = + open_credentialless_document.sjs + store_header.sjs +[browser_origin_trial_coep_credentialless_fetch_3.js] +support-files = + open_credentialless_document.sjs + store_header.sjs +[browser_origin_trial_coep_credentialless_worker.js] +support-files = + open_credentialless_document.sjs + store_header.sjs + credentialless_worker.sjs +[browser_origin_trial_coep_credentialless_cache.js] +support-files = + open_credentialless_document.sjs + credentialless_resource.sjs diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_cache.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_cache.js new file mode 100644 index 000000000000..851ab974190e --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_cache.js @@ -0,0 +1,142 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const USE_CREDENTIALLESS = true; +const NO_CREDENTIALLESS = false; + +const RESOURCE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://test1.example.com" + ) + "credentialless_resource.sjs"; + +async function store(storer, url, requestCredentialMode) { + await SpecialPowers.spawn( + storer.linkedBrowser, + [url, requestCredentialMode], + async function(url, requestCredentialMode) { + const cache = await content.caches.open("v1"); + const fetchRequest = new content.Request(url, { + mode: "no-cors", + credentials: requestCredentialMode, + }); + + const fetchResponse = await content.fetch(fetchRequest); + content.wrappedJSObject.console.log(fetchResponse.headers); + await cache.put(fetchRequest, fetchResponse); + } + ); +} + +async function retrieve(retriever, resourceURL) { + return await SpecialPowers.spawn( + retriever.linkedBrowser, + [resourceURL], + async function(url) { + const cache = await content.caches.open("v1"); + try { + await cache.match(url); + return "retrieved"; + } catch (error) { + return "error"; + } + } + ); +} + +async function testCache( + storer, + storeRequestCredentialMode, + resourceCOEP, + retriever, + expectation +) { + const resourceURL = RESOURCE_URL + "?" + resourceCOEP; + + await store(storer, resourceURL, storeRequestCredentialMode); + const result = await retrieve(retriever, resourceURL); + + is(result, expectation); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + const noneTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TOP_LEVEL_URL + ); + const requireCorpTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TOP_LEVEL_URL + "?requirecorp" + ); + const credentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TOP_LEVEL_URL + "?credentialless" + ); + + await testCache(noneTab, "include", "", noneTab, "retrieved"); + await testCache(noneTab, "include", "", credentiallessTab, "error"); + await testCache(noneTab, "omit", "", credentiallessTab, "retrieved"); + await testCache( + noneTab, + "include", + "corp_cross_origin", + credentiallessTab, + "retrieved" + ); + await testCache(noneTab, "include", "", requireCorpTab, "error"); + await testCache( + noneTab, + "include", + "corp_cross_origin", + requireCorpTab, + "retrieved" + ); + await testCache(credentiallessTab, "include", "", noneTab, "retrieved"); + await testCache( + credentiallessTab, + "include", + "", + credentiallessTab, + "retrieved" + ); + await testCache(credentiallessTab, "include", "", requireCorpTab, "error"); + await testCache( + requireCorpTab, + "include", + "corp_cross_origin", + noneTab, + "retrieved" + ); + await testCache( + requireCorpTab, + "include", + "corp_cross_origin", + credentiallessTab, + "retrieved" + ); + await testCache( + requireCorpTab, + "include", + "corp_cross_origin", + requireCorpTab, + "retrieved" + ); + + await BrowserTestUtils.removeTab(noneTab); + await BrowserTestUtils.removeTab(requireCorpTab); + await BrowserTestUtils.removeTab(credentiallessTab); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_1.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_1.js new file mode 100644 index 000000000000..c8c5c0078b83 --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_1.js @@ -0,0 +1,134 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const USE_CREDENTIALLESS = true; +const NO_CREDENTIALLESS = false; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function() { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResult +) { + let topLevelUrl = TOP_LEVEL_URL; + if (isCredentialless) { + topLevelUrl += "?credentialless"; + } + + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [ + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + GET_STATE_URL, + expectedCookieResult, + ], + async function( + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + getStateURL, + expectedCookieResult + ) { + // When store_header.sjs receives this request, it will store + // whether it has received the cookie as a shared state. + await content.fetch(fetchRequestURL, { + mode: fetchRequestMode, + credentials: fetchRequestCrendentials, + }); + + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + } + ); + + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function doTest( + origin, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + await testOrigin( + origin, + USE_CREDENTIALLESS, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForCredentialless + ); + await testOrigin( + origin, + NO_CREDENTIALLESS, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless + ); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + // Cookies never sent with omit + await doTest(SAME_ORIGIN, "no-cors", "omit", "noCookie", "noCookie"); + await doTest(SAME_ORIGIN, "cors", "omit", "noCookie", "noCookie"); + await doTest(CROSS_ORIGIN, "no-cors", "omit", "noCookie", "noCookie"); + await doTest(CROSS_ORIGIN, "cors", "omit", "noCookie", "noCookie"); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_2.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_2.js new file mode 100644 index 000000000000..b7817ec1e0a9 --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_2.js @@ -0,0 +1,134 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const USE_CREDENTIALLESS = true; +const NO_CREDENTIALLESS = false; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function() { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResult +) { + let topLevelUrl = TOP_LEVEL_URL; + if (isCredentialless) { + topLevelUrl += "?credentialless"; + } + + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [ + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + GET_STATE_URL, + expectedCookieResult, + ], + async function( + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + getStateURL, + expectedCookieResult + ) { + // When store_header.sjs receives this request, it will store + // whether it has received the cookie as a shared state. + await content.fetch(fetchRequestURL, { + mode: fetchRequestMode, + credentials: fetchRequestCrendentials, + }); + + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + } + ); + + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function doTest( + origin, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + await testOrigin( + origin, + USE_CREDENTIALLESS, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForCredentialless + ); + await testOrigin( + origin, + NO_CREDENTIALLESS, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless + ); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + // Same-origin request contains Cookies. + await doTest(SAME_ORIGIN, "no-cors", "include", "hasCookie", "hasCookie"); + await doTest(SAME_ORIGIN, "cors", "include", "hasCookie", "hasCookie"); + await doTest(SAME_ORIGIN, "no-cors", "same-origin", "hasCookie", "hasCookie"); + await doTest(SAME_ORIGIN, "cors", "same-origin", "hasCookie", "hasCookie"); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_3.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_3.js new file mode 100644 index 000000000000..53970a097fd2 --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_3.js @@ -0,0 +1,139 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const USE_CREDENTIALLESS = true; +const NO_CREDENTIALLESS = false; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function() { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResult +) { + let topLevelUrl = TOP_LEVEL_URL; + if (isCredentialless) { + topLevelUrl += "?credentialless"; + } + + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [ + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + GET_STATE_URL, + expectedCookieResult, + ], + async function( + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + getStateURL, + expectedCookieResult + ) { + // When store_header.sjs receives this request, it will store + // whether it has received the cookie as a shared state. + await content.fetch(fetchRequestURL, { + mode: fetchRequestMode, + credentials: fetchRequestCrendentials, + }); + + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + } + ); + + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function doTest( + origin, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + await testOrigin( + origin, + USE_CREDENTIALLESS, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForCredentialless + ); + await testOrigin( + origin, + NO_CREDENTIALLESS, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless + ); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + // Cross-origin CORS requests contains Cookies, if credentials mode is set to + // 'include'. This does not depends on COEP. + await doTest(CROSS_ORIGIN, "cors", "include", "hasCookie", "hasCookie"); + await doTest(CROSS_ORIGIN, "cors", "same-origin", "noCookie", "noCookie"); + + // Cross-origin no-CORS requests includes Cookies when: + // 1. credentials mode is 'include' + // 2. COEP: is not credentialless. + await doTest(CROSS_ORIGIN, "no-cors", "include", "hasCookie", "noCookie"); + await doTest(CROSS_ORIGIN, "no-cors", "same-origin", "noCookie", "noCookie"); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_worker.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_worker.js new file mode 100644 index 000000000000..8afe9149df22 --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_worker.js @@ -0,0 +1,164 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const WORKER_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "credentialless_worker.sjs"; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const WORKER_USES_CREDENTIALLESS = "credentialless"; +const WORKER_NOT_USE_CREDENTIALLESS = ""; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function() { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + workerUsesCredentialless, + expectedCookieResult +) { + let topLevelUrl = TOP_LEVEL_URL; + if (isCredentialless) { + topLevelUrl += "?credentialless"; + } + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + let workerScriptURL = WORKER_URL + "?" + workerUsesCredentialless; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [fetchRequestURL, GET_STATE_URL, workerScriptURL, expectedCookieResult], + async function( + fetchRequestURL, + getStateURL, + workerScriptURL, + expectedCookieResult + ) { + const worker = new content.Worker(workerScriptURL, {}); + + // When the worker receives this message, it'll send + // a fetch request to fetchRequestURL, and fetchRequestURL + // will store whether it has received the cookie as a + // shared state. + worker.postMessage(fetchRequestURL); + + if (expectedCookieResult == "error") { + await new Promise(r => { + worker.onerror = function() { + ok(true, "worker has error"); + r(); + }; + }); + } else { + await new Promise(r => { + worker.addEventListener("message", async function() { + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + r(); + }); + }); + } + } + ); + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function dedicatedWorkerTest( + origin, + workerCOEP, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + await testOrigin( + origin, + false, + workerCOEP, + expectedCookieResultForNoCredentialless + ); + await testOrigin( + origin, + true, + workerCOEP, + expectedCookieResultForCredentialless + ); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], // Explicitly set credentialless to false because we want to test origin trial + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + await dedicatedWorkerTest( + SAME_ORIGIN, + WORKER_NOT_USE_CREDENTIALLESS, + "hasCookie", + "error" + ); + await dedicatedWorkerTest( + SAME_ORIGIN, + WORKER_USES_CREDENTIALLESS, + "hasCookie", + "hasCookie" + ); + + await dedicatedWorkerTest( + CROSS_ORIGIN, + WORKER_NOT_USE_CREDENTIALLESS, + "hasCookie", + "error" + ); + await dedicatedWorkerTest( + CROSS_ORIGIN, + WORKER_USES_CREDENTIALLESS, + "noCookie", + "noCookie" + ); +}); diff --git a/dom/fetch/tests/credentialless_resource.sjs b/dom/fetch/tests/credentialless_resource.sjs new file mode 100644 index 000000000000..72d0d738ecdc --- /dev/null +++ b/dom/fetch/tests/credentialless_resource.sjs @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function handleRequest(request, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: image/png\r\n"); + if (request.queryString === "corp_cross_origin") { + response.write("Cross-Origin-Resource-Policy: cross-origin\r\n"); + } + response.write("\r\n"); + response.write(IMG_BYTES); + response.finish(); +} diff --git a/dom/fetch/tests/credentialless_worker.sjs b/dom/fetch/tests/credentialless_worker.sjs new file mode 100644 index 000000000000..a9e2197d1843 --- /dev/null +++ b/dom/fetch/tests/credentialless_worker.sjs @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const WORKER = ` + onmessage = function(event) { + fetch(event.data, { + mode: "no-cors", + credentials: "include" + }).then(function() { + postMessage("fetch done"); + }); + } +`; + +function handleRequest(request, response) { + if (request.queryString === "credentialless") { + response.setHeader("Cross-Origin-Embedder-Policy", "credentialless", true); + } + + response.setHeader("Content-Type", "application/javascript", false); + response.setStatusLine(request.httpVersion, "200", "Found"); + response.write(WORKER); +} diff --git a/dom/fetch/tests/open_credentialless_document.sjs b/dom/fetch/tests/open_credentialless_document.sjs new file mode 100644 index 000000000000..d01a3bb6a39d --- /dev/null +++ b/dom/fetch/tests/open_credentialless_document.sjs @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const HTML = ` +
+ + + + + Hello World +`; + +function handleRequest(request, response) { + if (request.queryString == "credentialless") { + response.setHeader("Cross-Origin-Embedder-Policy", "credentialless"); + } else if (request.queryString === "requirecorp") { + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + } + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setStatusLine(request.httpVersion, "200", "Found"); + response.write(HTML); +} diff --git a/dom/fetch/tests/store_header.sjs b/dom/fetch/tests/store_header.sjs new file mode 100644 index 000000000000..3290a09995f4 --- /dev/null +++ b/dom/fetch/tests/store_header.sjs @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const key = "store_header"; +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + + if (request.queryString === "getstate") { + response.write(getSharedState(key)); + } else if (request.queryString === "checkheader") { + if (request.hasHeader("Cookie")) { + setSharedState(key, "hasCookie"); + } else { + setSharedState(key, "noCookie"); + } + } else { + // This is the first request which sets the cookie + } +} diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl index 579043a768c5..51e3288c4cc8 100644 --- a/dom/interfaces/base/nsIDOMWindowUtils.idl +++ b/dom/interfaces/base/nsIDOMWindowUtils.idl @@ -2211,6 +2211,8 @@ interface nsIDOMWindowUtils : nsISupports { */ void resetMobileViewportManager(); + bool isCoepCredentialless(); + /** * NOTE: Currently works only on GTK+. */ diff --git a/dom/origin-trials/OriginTrials.cpp b/dom/origin-trials/OriginTrials.cpp index c8377ea036d2..e9c4eaddcbd8 100644 --- a/dom/origin-trials/OriginTrials.cpp +++ b/dom/origin-trials/OriginTrials.cpp @@ -212,6 +212,8 @@ static int32_t PrefState(OriginTrial aTrial) { return StaticPrefs::dom_origin_trials_test_trial_state(); case OriginTrial::OffscreenCanvas: return StaticPrefs::dom_origin_trials_offscreen_canvas_state(); + case OriginTrial::CoepCredentialless: + return StaticPrefs::dom_origin_trials_coep_credentialless_state(); case OriginTrial::MAX: MOZ_ASSERT_UNREACHABLE("Unknown trial!"); break; @@ -219,13 +221,7 @@ static int32_t PrefState(OriginTrial aTrial) { return 0; } -bool OriginTrials::IsEnabled(JSContext* aCx, JSObject* aObject, - OriginTrial aTrial) { - if (nsContentUtils::ThreadsafeIsSystemCaller(aCx)) { - return true; - } - LOG("OriginTrials::IsEnabled(%d)\n", int(aTrial)); - +bool OriginTrials::IsEnabled(OriginTrial aTrial) const { switch (PrefState(aTrial)) { case 1: return true; @@ -235,6 +231,15 @@ bool OriginTrials::IsEnabled(JSContext* aCx, JSObject* aObject, break; } + return mEnabledTrials.contains(aTrial); +} + +bool OriginTrials::IsEnabled(JSContext* aCx, JSObject* aObject, + OriginTrial aTrial) { + if (nsContentUtils::ThreadsafeIsSystemCaller(aCx)) { + return true; + } + LOG("OriginTrials::IsEnabled(%d)\n", int(aTrial)); nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx); MOZ_ASSERT(global); return global && global->Trials().IsEnabled(aTrial); diff --git a/dom/origin-trials/OriginTrials.h b/dom/origin-trials/OriginTrials.h index 5f62c39b75ab..d1de36a20267 100644 --- a/dom/origin-trials/OriginTrials.h +++ b/dom/origin-trials/OriginTrials.h @@ -40,9 +40,7 @@ class OriginTrials final { void UpdateFromToken(const nsAString& aBase64EncodedToken, nsIPrincipal* aPrincipal); - bool IsEnabled(OriginTrial aTrial) const { - return mEnabledTrials.contains(aTrial); - } + bool IsEnabled(OriginTrial aTrial) const; // Checks whether a given origin trial is enabled for a given call. static bool IsEnabled(JSContext*, JSObject*, OriginTrial); diff --git a/dom/origin-trials/ffi/lib.rs b/dom/origin-trials/ffi/lib.rs index b28fc659ff8b..f850fe52458d 100644 --- a/dom/origin-trials/ffi/lib.rs +++ b/dom/origin-trials/ffi/lib.rs @@ -10,6 +10,7 @@ pub enum OriginTrial { // NOTE(emilio): 0 is reserved for WebIDL usage. TestTrial = 1, OffscreenCanvas = 2, + CoepCredentialless = 3, MAX, } @@ -19,6 +20,7 @@ impl OriginTrial { Some(match s { "TestTrial" => Self::TestTrial, "OffscreenCanvas" => Self::OffscreenCanvas, + "CoepCredentialless" => Self::CoepCredentialless, _ => return None, }) } diff --git a/dom/origin-trials/tests/mochitest.ini b/dom/origin-trials/tests/mochitest.ini index 4c1103a1f588..68b834f5e035 100644 --- a/dom/origin-trials/tests/mochitest.ini +++ b/dom/origin-trials/tests/mochitest.ini @@ -2,6 +2,7 @@ prefs = dom.origin-trials.enabled=true dom.origin-trials.test-key.enabled=true + browser.tabs.remote.coep.credentialless=false support-files = test_header_simple.html^headers^ common.js diff --git a/dom/origin-trials/tests/test_meta_simple.html b/dom/origin-trials/tests/test_meta_simple.html index 979f497c84bc..9afd4a645c16 100644 --- a/dom/origin-trials/tests/test_meta_simple.html +++ b/dom/origin-trials/tests/test_meta_simple.html @@ -3,6 +3,8 @@ + + diff --git a/dom/workers/loader/CacheLoadHandler.cpp b/dom/workers/loader/CacheLoadHandler.cpp index 872dd7a3ce2f..c1ea3c29bd68 100644 --- a/dom/workers/loader/CacheLoadHandler.cpp +++ b/dom/workers/loader/CacheLoadHandler.cpp @@ -382,7 +382,9 @@ void CacheLoadHandler::ResolvedCallback(JSContext* aCx, headers->Get("cross-origin-embedder-policy"_ns, coepHeader, IgnoreErrors()); nsILoadInfo::CrossOriginEmbedderPolicy coep = - NS_GetCrossOriginEmbedderPolicyFromHeader(coepHeader); + NS_GetCrossOriginEmbedderPolicyFromHeader( + coepHeader, + mWorkerPrivate->Trials().IsEnabled(OriginTrial::CoepCredentialless)); rv = ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( mWorkerPrivate, coep, mLoader->IsMainScript()); diff --git a/dom/workers/loader/ScriptResponseHeaderProcessor.cpp b/dom/workers/loader/ScriptResponseHeaderProcessor.cpp index 06bcf91b2426..7b844d66011c 100644 --- a/dom/workers/loader/ScriptResponseHeaderProcessor.cpp +++ b/dom/workers/loader/ScriptResponseHeaderProcessor.cpp @@ -49,7 +49,9 @@ nsresult ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( } nsILoadInfo::CrossOriginEmbedderPolicy coep; - MOZ_TRY(httpChannel->GetResponseEmbedderPolicy(&coep)); + MOZ_TRY(httpChannel->GetResponseEmbedderPolicy( + mWorkerPrivate->Trials().IsEnabled(OriginTrial::CoepCredentialless), + &coep)); return ProcessCrossOriginEmbedderPolicyHeader(mWorkerPrivate, coep, mIsMainScript); diff --git a/ipc/glue/BackgroundUtils.cpp b/ipc/glue/BackgroundUtils.cpp index df63b2b4b123..b7a0e54a0515 100644 --- a/ipc/glue/BackgroundUtils.cpp +++ b/ipc/glue/BackgroundUtils.cpp @@ -537,7 +537,9 @@ nsresult LoadInfoToLoadInfoArgs(nsILoadInfo* aLoadInfo, aLoadInfo->GetIsFromObjectOrEmbed(), cookieJarSettingsArgs, aLoadInfo->GetRequestBlockingReason(), maybeCspToInheritInfo, aLoadInfo->GetStoragePermission(), aLoadInfo->GetIsMetaRefresh(), - aLoadInfo->GetLoadingEmbedderPolicy(), unstrippedURI)); + aLoadInfo->GetLoadingEmbedderPolicy(), + aLoadInfo->GetIsOriginTrialCoepCredentiallessEnabledForTopLevel(), + unstrippedURI)); return NS_OK; } @@ -779,7 +781,9 @@ nsresult LoadInfoArgsToLoadInfo( loadInfoArgs.isInDevToolsContext(), loadInfoArgs.parserCreatedScript(), loadInfoArgs.storagePermission(), loadInfoArgs.isMetaRefresh(), loadInfoArgs.requestBlockingReason(), loadingContext, - loadInfoArgs.loadingEmbedderPolicy(), loadInfoArgs.unstrippedURI()); + loadInfoArgs.loadingEmbedderPolicy(), + loadInfoArgs.originTrialCoepCredentiallessEnabledForTopLevel(), + loadInfoArgs.unstrippedURI()); if (loadInfoArgs.isFromProcessingFrameAttributes()) { loadInfo->SetIsFromProcessingFrameAttributes(); diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml index d199f545c668..32f36680958f 100644 --- a/modules/libpref/init/StaticPrefList.yaml +++ b/modules/libpref/init/StaticPrefList.yaml @@ -1542,6 +1542,7 @@ type: RelaxedAtomicBool value: @IS_NIGHTLY_BUILD@ mirror: always + do_not_use_directly: true # When this pref is enabled top level loads with a mismatched # Cross-Origin-Opener-Policy header will be loaded in a separate process. @@ -2997,6 +2998,13 @@ value: 0 mirror: always +# Origin trial state for COEP: Credentialless. +# 0: normal, 1: always-enabled, 2: always-disabled +- name: dom.origin-trials.coep-credentialless.state + type: RelaxedAtomicInt32 + value: 0 + mirror: always + # Is support for Window.paintWorklet enabled? - name: dom.paintWorklet.enabled type: bool diff --git a/netwerk/base/LoadInfo.cpp b/netwerk/base/LoadInfo.cpp index 9277c804444a..817102b2c534 100644 --- a/netwerk/base/LoadInfo.cpp +++ b/netwerk/base/LoadInfo.cpp @@ -513,6 +513,11 @@ LoadInfo::LoadInfo(dom::WindowGlobalParent* aParentWGP, RefPtr