diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/security/test/https-only | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
51 files changed, 3359 insertions, 0 deletions
diff --git a/dom/security/test/https-only/browser.toml b/dom/security/test/https-only/browser.toml new file mode 100644 index 0000000000..2cba418aff --- /dev/null +++ b/dom/security/test/https-only/browser.toml @@ -0,0 +1,62 @@ +[DEFAULT] +prefs = ["dom.security.https_first=false"] + +["browser_background_redirect.js"] +support-files = ["file_background_redirect.sjs"] + +["browser_bug1874801.js"] +support-files = [ + "file_bug1874801.sjs", + "file_bug1874801.html", +] + +["browser_console_logging.js"] +support-files = ["file_console_logging.html"] + +["browser_continue_button_delay.js"] + +["browser_cors_mixedcontent.js"] +support-files = ["file_cors_mixedcontent.html"] + +["browser_hsts_host.js"] +support-files = [ + "hsts_headers.sjs", + "file_fragment_noscript.html", +] + +["browser_httpsonly_prefs.js"] + +["browser_httpsonly_speculative_connect.js"] +support-files = ["file_httpsonly_speculative_connect.html"] + +["browser_iframe_test.js"] +skip-if = [ + "os == 'linux' && bits == 64", # Bug 1735565 + "os == 'win' && bits == 64", # Bug 1735565 +] +support-files = ["file_iframe_test.sjs"] + +["browser_navigation.js"] +support-files = ["file_redirect_to_insecure.sjs"] + +["browser_redirect_tainting.js"] +support-files = ["file_redirect_tainting.sjs"] + +["browser_save_as.js"] +support-files = ["file_save_as.html"] + +["browser_triggering_principal_exemption.js"] + +["browser_upgrade_exceptions.js"] + +["browser_upgrade_exemption.js"] + +["browser_user_gesture.js"] +support-files = ["file_user_gesture.html"] + +["browser_websocket_exceptions.js"] +skip-if = ["os == 'android'"] # WebSocket tests are not supported on Android Yet. Bug 1566168. +support-files = [ + "file_websocket_exceptions.html", + "file_websocket_exceptions_iframe.html", +] diff --git a/dom/security/test/https-only/browser_background_redirect.js b/dom/security/test/https-only/browser_background_redirect.js new file mode 100644 index 0000000000..2907278943 --- /dev/null +++ b/dom/security/test/https-only/browser_background_redirect.js @@ -0,0 +1,64 @@ +"use strict"; +/* Description of the test: + * We load a page which gets upgraded to https. HTTPS-Only Mode then + * sends an 'http' background request which we redirect (using the + * web extension API) to 'same-origin' https. We ensure the HTTPS-Only + * Error page does not occur, but we https page gets loaded. + */ + +let extension = null; + +add_task(async function test_https_only_background_request_redirect() { + // A longer timeout is necessary for this test since we have to wait + // at least 3 seconds for the https-only background request to happen. + requestLongerTimeout(10); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "http://example.com/*"], + }, + background() { + let { browser } = this; + browser.webRequest.onBeforeRequest.addListener( + details => { + if (details.url === "http://example.com/") { + browser.test.sendMessage("redir-handled"); + let redirectUrl = "https://example.com/"; + return { redirectUrl }; + } + return undefined; + }, + { urls: ["http://example.com/*"] }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + let loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + BrowserTestUtils.startLoadingURIString( + browser, + "http://example.com/browser/dom/security/test/https-only/file_background_redirect.sjs?start" + ); + + await extension.awaitMessage("redir-handled"); + + await loaded; + + await SpecialPowers.spawn(browser, [], async function () { + let innerHTML = content.document.body.innerHTML; + ok( + innerHTML.includes("Test Page for Bug 1683015 loaded"), + "No https-only error page" + ); + }); + }); + + await extension.unload(); +}); diff --git a/dom/security/test/https-only/browser_bug1874801.js b/dom/security/test/https-only/browser_bug1874801.js new file mode 100644 index 0000000000..280a072736 --- /dev/null +++ b/dom/security/test/https-only/browser_bug1874801.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Specifically test https://bugzilla.mozilla.org/show_bug.cgi?id=1874801 + +const TAB_URL = + "https://example.com/browser/dom/security/test/https-only/file_bug1874801.html"; + +function assertImageLoaded(tab) { + return ContentTask.spawn(tab.linkedBrowser, {}, () => { + const img = content.document.getElementsByTagName("img")[0]; + + ok(!!img, "Image tag should exist"); + ok(img.complete && img.naturalWidth > 0, "Image should have loaded "); + }); +} + +add_task(async function test_bug1874801() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.mixed_content.upgrade_display_content", false], + ["dom.security.https_first", true], + ["dom.security.https_only_mode", true], + ], + }); + + // Open Tab + const tabToClose = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TAB_URL, + true + ); + + // Make sure the image was loaded via HTTPS + await assertImageLoaded(tabToClose); + + // Close Tab + const tabClosePromise = + BrowserTestUtils.waitForSessionStoreUpdate(tabToClose); + BrowserTestUtils.removeTab(tabToClose); + await tabClosePromise; + + // Restore Tab + const restoredTabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + TAB_URL, + true + ); + undoCloseTab(); + const restoredTab = await restoredTabPromise; + + // Make sure the image was loaded via HTTPS + await assertImageLoaded(restoredTab); +}); diff --git a/dom/security/test/https-only/browser_console_logging.js b/dom/security/test/https-only/browser_console_logging.js new file mode 100644 index 0000000000..d648c27289 --- /dev/null +++ b/dom/security/test/https-only/browser_console_logging.js @@ -0,0 +1,153 @@ +// Bug 1625448 - HTTPS Only Mode - Tests for console logging +// https://bugzilla.mozilla.org/show_bug.cgi?id=1625448 +// This test makes sure that the various console messages from the HTTPS-Only +// mode get logged to the console. +"use strict"; + +// Test Cases +// description: Description of what the subtests expects. +// expectLogLevel: Expected log-level of a message. +// expectIncludes: Expected substrings the message should contain. +let tests = [ + { + description: "Top-Level upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "HTTPS-Only Mode: Upgrading insecure request", + "to use", + "file_console_logging.html", + ], + }, + { + description: "iFrame upgrade failure should get logged", + expectLogLevel: Ci.nsIConsoleMessage.error, + expectIncludes: [ + "HTTPS-Only Mode: Upgrading insecure request", + "failed", + "file_console_logging.html", + ], + }, + { + description: "WebSocket upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "HTTPS-Only Mode: Upgrading insecure request", + "to use", + "ws://does.not.exist", + ], + }, + { + description: "Sub-Resource upgrade for file_1 should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure", "request", "file_1.jpg"], + }, + { + description: "Sub-Resource upgrade for file_2 should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: ["Upgrading insecure", "request", "to use", "file_2.jpg"], + }, + { + description: "Exempt request for file_exempt should get logged", + expectLogLevel: Ci.nsIConsoleMessage.info, + expectIncludes: [ + "Not upgrading insecure request", + "because it is exempt", + "file_exempt.jpg", + ], + }, + { + description: "Sub-Resource upgrade failure for file_2 should get logged", + expectLogLevel: Ci.nsIConsoleMessage.error, + expectIncludes: ["Upgrading insecure request", "failed", "file_2.jpg"], + }, +]; + +const testPathUpgradeable = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); +// DNS errors are not logged as HTTPS-Only Mode upgrade failures, so we have to +// upgrade to a domain that exists but fails. +const testPathNotUpgradeable = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://self-signed.example.com" +); +const kTestURISuccess = testPathUpgradeable + "file_console_logging.html"; +const kTestURIFail = testPathNotUpgradeable + "file_console_logging.html"; +const kTestURIExempt = testPathUpgradeable + "file_exempt.jpg"; + +const UPGRADE_DISPLAY_CONTENT = + "security.mixed_content.upgrade_display_content"; + +add_task(async function () { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + Services.console.registerListener(on_new_message); + // 1. Upgrade page to https:// + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + kTestURISuccess + ); + // 2. Make an exempt http:// request + let xhr = new XMLHttpRequest(); + xhr.open("GET", kTestURIExempt, true); + xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; + xhr.send(); + // 3. Make Websocket request + new WebSocket("ws://does.not.exist"); + + await BrowserTestUtils.waitForCondition(() => tests.length === 0); + + // Clean up + Services.console.unregisterListener(on_new_message); +}); + +function on_new_message(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + // Bools about message and pref + const isMCL2Enabled = Services.prefs.getBoolPref(UPGRADE_DISPLAY_CONTENT); + const isHTTPSOnlyModeLog = message.includes("HTTPS-Only Mode:"); + const isMCLog = message.includes("Mixed Content:"); + + // Check for messages about HTTPS-only upgrades (those should be unrelated to mixed content upgrades) + // or for mixed content upgrades which should only occur if security.mixed_content.upgrade_display_content is enabled + // (unrelated to https-only logs). + if ( + (isHTTPSOnlyModeLog && !isMCLog) || + (isMCLog && isMCL2Enabled && !isHTTPSOnlyModeLog) + ) { + for (let i = 0; i < tests.length; i++) { + const testCase = tests[i]; + // If security.mixed_content.upgrade_display_content is enabled, the mixed content control mechanism is upgrading file2.jpg + // and HTTPS-Only mode is not failing upgrading file2.jpg, so it won't be logged. + // so skip last test case + if ( + testCase.description == + "Sub-Resource upgrade failure for file_2 should get logged" && + isMCL2Enabled + ) { + tests.splice(i, 1); + continue; + } + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + tests.splice(i, 1); + break; + } + } +} diff --git a/dom/security/test/https-only/browser_continue_button_delay.js b/dom/security/test/https-only/browser_continue_button_delay.js new file mode 100644 index 0000000000..6bdee1610e --- /dev/null +++ b/dom/security/test/https-only/browser_continue_button_delay.js @@ -0,0 +1,59 @@ +"use strict"; + +function waitForEnabledButton() { + return new Promise(resolve => { + const button = content.document.getElementById("openInsecure"); + const observer = new content.MutationObserver(mutations => { + for (const mutation of mutations) { + if ( + mutation.type === "attributes" && + mutation.attributeName === "inert" && + !mutation.target.inert + ) { + resolve(); + } + } + }); + observer.observe(button, { attributeFilter: ["inert"] }); + ok( + button.inert, + "The 'Continue to HTTP Site' button should be inert right after the error page is loaded." + ); + }); +} + +add_task(async function () { + waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + const specifiedDelay = Services.prefs.getIntPref( + "security.dialog_enable_delay", + 1000 + ); + + let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser); + info("Loading insecure page"); + const startTime = Date.now(); + BrowserTestUtils.startLoadingURIString( + gBrowser, + // We specifically want a insecure url here that will fail to upgrade + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://untrusted.example.com:80" + ); + await loaded; + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], waitForEnabledButton); + const endTime = Date.now(); + + const observedDelay = endTime - startTime; + + Assert.greater( + observedDelay, + specifiedDelay - 100, + `The observed delay (${observedDelay}ms) should be roughly the same or greater than the delay specified in "security.dialog_enable_delay" (${specifiedDelay}ms)` + ); + + finish(); +}); diff --git a/dom/security/test/https-only/browser_cors_mixedcontent.js b/dom/security/test/https-only/browser_cors_mixedcontent.js new file mode 100644 index 0000000000..fb78d66979 --- /dev/null +++ b/dom/security/test/https-only/browser_cors_mixedcontent.js @@ -0,0 +1,127 @@ +// Bug 1659505 - Https-Only: CORS and MixedContent tests +// https://bugzilla.mozilla.org/bug/1659505 +"use strict"; + +// > How does this test work? +// We open a page, that makes two fetch-requests to example.com (same-origin) +// and example.org (cross-origin). When both fetch-calls have either failed or +// succeeded, the site dispatches an event with the results. + +add_task(async function () { + // HTTPS-Only Mode disabled + await runTest({ + description: "Load site with HTTP and HOM disabled", + topLevelScheme: "http", + + expectedSameOrigin: "success", // ok + expectedCrossOrigin: "error", // CORS + }); + await runTest({ + description: "Load site with HTTPS and HOM disabled", + topLevelScheme: "https", + + expectedSameOrigin: "error", // Mixed Content + expectedCrossOrigin: "error", // Mixed Content + }); + + // HTTPS-Only Mode disabled and MixedContent blocker disabled + await SpecialPowers.pushPrefEnv({ + set: [["security.mixed_content.block_active_content", false]], + }); + await runTest({ + description: "Load site with HTTPS; HOM and MixedContent blocker disabled", + topLevelScheme: "https", + + expectedSameOrigin: "error", // CORS + expectedCrossOrigin: "error", // CORS + }); + await SpecialPowers.popPrefEnv(); + + // HTTPS-Only Mode enabled, no exception + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + await runTest({ + description: "Load site with HTTP and HOM enabled", + topLevelScheme: "http", + + expectedSameOrigin: "success", // ok + expectedCrossOrigin: "error", // CORS + }); + + // HTTPS-Only enabled, with exception + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://example.com", + }, + ]); + + await runTest({ + description: "Load site with HTTP, HOM enabled but site exempt", + topLevelScheme: "http", + + expectedSameOrigin: "success", // ok + expectedCrossOrigin: "error", // CORS + }); + + await SpecialPowers.popPermissions(); + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "https://example.com", + }, + ]); + await runTest({ + description: "Load site with HTTPS, HOM enabled but site exempt", + topLevelScheme: "https", + + expectedSameOrigin: "error", // Mixed Content + expectedCrossOrigin: "error", // Mixed Content + }); + + // Remove permission again (has to be done manually for some reason?) + await SpecialPowers.popPermissions(); +}); + +const SERVER_URL = scheme => + `${scheme}://example.com/browser/dom/security/test/https-only/file_cors_mixedcontent.html`; + +async function runTest(test) { + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + let loaded = BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.startLoadingURIString( + browser, + SERVER_URL(test.topLevelScheme) + ); + + await loaded; + + // eslint-disable-next-line no-shadow + await SpecialPowers.spawn(browser, [test], async function (test) { + const promise = new Promise(resolve => { + content.addEventListener("FetchEnded", resolve, { + once: true, + }); + }); + + content.dispatchEvent(new content.Event("StartFetch")); + + const { detail } = await promise; + + is( + detail.comResult, + test.expectedSameOrigin, + `${test.description} (same-origin)` + ); + is( + detail.orgResult, + test.expectedCrossOrigin, + `${test.description} (cross-origin)` + ); + }); + }); +} diff --git a/dom/security/test/https-only/browser_hsts_host.js b/dom/security/test/https-only/browser_hsts_host.js new file mode 100644 index 0000000000..858c19865c --- /dev/null +++ b/dom/security/test/https-only/browser_hsts_host.js @@ -0,0 +1,203 @@ +// Bug 1722489 - HTTPS-Only Mode - Tests evaluation order +// https://bugzilla.mozilla.org/show_bug.cgi?id=1722489 +// This test ensures that an http request to an hsts host +// gets upgraded by hsts and not by https-only. +"use strict"; + +// Set bools to track that tests ended. +let readMessage = false; +let testFinished = false; +// Visit a secure site that sends an HSTS header to set up the rest of the +// test. +add_task(async function see_hsts_header() { + let setHstsUrl = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "hsts_headers.sjs"; + Services.obs.addObserver(observer, "http-on-examine-response"); + + let promiseLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + setHstsUrl + ); + BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, setHstsUrl); + await promiseLoaded; + + await BrowserTestUtils.waitForCondition(() => readMessage); + // Clean up + Services.obs.removeObserver(observer, "http-on-examine-response"); +}); + +// Test that HTTPS_Only is not performed if HSTS host is visited. +add_task(async function () { + // A longer timeout is necessary for this test than the plain mochitests + // due to opening a new tab with the web console. + requestLongerTimeout(4); + + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + Services.console.registerListener(onNewMessage); + const RESOURCE_LINK = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" + ) + "hsts_headers.sjs"; + + // 1. Upgrade page to https:// + let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + RESOURCE_LINK + ); + await promiseLoaded; + + await BrowserTestUtils.waitForCondition(() => testFinished); + + // Clean up + Services.console.unregisterListener(onNewMessage); + + await SpecialPowers.popPrefEnv(); +}); + +// Test that when clicking on #fragment with a different scheme (http vs https) +// DOES cause an actual navigation with HSTS, even though https-only mode is +// enabled. +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", true], + [ + "dom.security.https_only_mode_break_upgrade_downgrade_endless_loop", + false, + ], + ], + }); + + const TEST_PAGE = + "http://example.com/browser/dom/security/test/https-only/file_fragment_noscript.html"; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_PAGE, + waitForLoad: true, + }, + async function (browser) { + const UPGRADED_URL = TEST_PAGE.replace("http:", "https:"); + + await SpecialPowers.spawn(browser, [UPGRADED_URL], async function (url) { + is(content.window.location.href, url); + + content.window.addEventListener("scroll", () => { + ok(false, "scroll event should not trigger"); + }); + + let beforeUnload = new Promise(resolve => { + content.window.addEventListener("beforeunload", resolve, { + once: true, + }); + }); + + content.window.document.querySelector("#clickMeButton").click(); + + // Wait for unload event. + await beforeUnload; + }); + + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [UPGRADED_URL], async function (url) { + is(content.window.location.href, url + "#foo"); + }); + } + ); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function () { + // Reset HSTS header + readMessage = false; + let clearHstsUrl = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "hsts_headers.sjs?reset"; + + Services.obs.addObserver(observer, "http-on-examine-response"); + // reset hsts header + let promiseLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + clearHstsUrl + ); + await BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + clearHstsUrl + ); + await promiseLoaded; + await BrowserTestUtils.waitForCondition(() => readMessage); + // Clean up + Services.obs.removeObserver(observer, "http-on-examine-response"); +}); + +function observer(subject, topic, state) { + info("observer called with " + topic); + if (topic == "http-on-examine-response") { + onExamineResponse(subject); + } +} + +function onExamineResponse(subject) { + let channel = subject.QueryInterface(Ci.nsIHttpChannel); + // If message was already read or is not related to "example.com", + // don't examine it. + if (!channel.URI.spec.includes("example.com") || readMessage) { + return; + } + info("onExamineResponse with " + channel.URI.spec); + if (channel.URI.spec.includes("reset")) { + try { + let hsts = channel.getResponseHeader("Strict-Transport-Security"); + is(hsts, "max-age=0", "HSTS header is not set"); + } catch (e) { + ok(false, "HSTS header still set"); + } + readMessage = true; + return; + } + try { + let hsts = channel.getResponseHeader("Strict-Transport-Security"); + let csp = channel.getResponseHeader("Content-Security-Policy"); + // Check that HSTS and CSP upgrade headers are set + is(hsts, "max-age=60", "HSTS header is set"); + is(csp, "upgrade-insecure-requests", "CSP header is set"); + } catch (e) { + ok(false, "No header set"); + } + readMessage = true; +} + +function onNewMessage(msgObj) { + const message = msgObj.message; + // ensure that request is not upgraded HTTPS-Only. + if (message.includes("Upgrading insecure request")) { + ok(false, "Top-Level upgrade shouldn't get logged"); + testFinished = true; + } else if ( + message.includes("Upgrading insecure speculative TCP connection") + ) { + // TODO: Check assertion + // https://bugzilla.mozilla.org/show_bug.cgi?id=1735683 + ok(true, "Top-Level upgrade shouldn't get logged"); + testFinished = true; + } else if (gBrowser.selectedBrowser.currentURI.scheme === "https") { + ok(true, "Top-Level upgrade shouldn't get logged"); + testFinished = true; + } +} diff --git a/dom/security/test/https-only/browser_httpsonly_prefs.js b/dom/security/test/https-only/browser_httpsonly_prefs.js new file mode 100644 index 0000000000..467ab490e7 --- /dev/null +++ b/dom/security/test/https-only/browser_httpsonly_prefs.js @@ -0,0 +1,118 @@ +"use strict"; + +async function runPrefTest( + aHTTPSOnlyPref, + aHTTPSOnlyPrefPBM, + aExecuteFromPBM, + aDesc, + aAssertURLStartsWith +) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", aHTTPSOnlyPref], + ["dom.security.https_only_mode_pbm", aHTTPSOnlyPrefPBM], + ], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + await ContentTask.spawn( + browser, + { aExecuteFromPBM, aDesc, aAssertURLStartsWith }, + // eslint-disable-next-line no-shadow + async function ({ aExecuteFromPBM, aDesc, aAssertURLStartsWith }) { + const responseURL = await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.timeout = 1200; + xhr.open("GET", "http://example.com"); + if (aExecuteFromPBM) { + xhr.channel.loadInfo.originAttributes = { + privateBrowsingId: 1, + }; + } + xhr.onreadystatechange = () => { + // We don't care about the result and it's possible that + // the requests might even succeed in some testing environments + if ( + xhr.readyState !== XMLHttpRequest.OPENED || + xhr.readyState !== XMLHttpRequest.UNSENT + ) { + // Let's make sure this function does not get called anymore + xhr.onreadystatechange = undefined; + resolve(xhr.responseURL); + } + }; + xhr.send(); + }); + ok(responseURL.startsWith(aAssertURLStartsWith), aDesc); + } + ); + }); +} + +add_task(async function () { + requestLongerTimeout(2); + + await runPrefTest( + false, + false, + false, + "Setting no prefs should not upgrade", + "http://" + ); + + await runPrefTest( + true, + false, + false, + "Setting aHTTPSOnlyPref should upgrade", + "https://" + ); + + await runPrefTest( + false, + true, + false, + "Setting aHTTPSOnlyPrefPBM should not upgrade", + "http://" + ); + + await runPrefTest( + false, + false, + true, + "Setting aPBM should not upgrade", + "http://" + ); + + await runPrefTest( + true, + true, + false, + "Setting aHTTPSOnlyPref and aHTTPSOnlyPrefPBM should should upgrade", + "https://" + ); + + await runPrefTest( + true, + false, + true, + "Setting aHTTPSOnlyPref and aPBM should upgrade", + "https://" + ); + + await runPrefTest( + false, + true, + true, + "Setting aHTTPSOnlyPrefPBM and aPBM should upgrade", + "https://" + ); + + await runPrefTest( + true, + true, + true, + "Setting aHTTPSOnlyPref and aHTTPSOnlyPrefPBM and aPBM should upgrade", + "https://" + ); +}); diff --git a/dom/security/test/https-only/browser_httpsonly_speculative_connect.js b/dom/security/test/https-only/browser_httpsonly_speculative_connect.js new file mode 100644 index 0000000000..d48f27b1a9 --- /dev/null +++ b/dom/security/test/https-only/browser_httpsonly_speculative_connect.js @@ -0,0 +1,71 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.org" +); + +let console_messages = [ + { + description: "Speculative Connection should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure speculative TCP connection", + "to use", + "example.org", + "file_httpsonly_speculative_connect.html", + ], + }, + { + description: "Upgrade should get logged", + expectLogLevel: Ci.nsIConsoleMessage.warn, + expectIncludes: [ + "Upgrading insecure request", + "to use", + "example.org", + "file_httpsonly_speculative_connect.html", + ], + }, +]; + +function on_new_console_messages(msgObj) { + const message = msgObj.message; + const logLevel = msgObj.logLevel; + + if (message.includes("HTTPS-Only Mode:")) { + for (let i = 0; i < console_messages.length; i++) { + const testCase = console_messages[i]; + // Check if log-level matches + if (logLevel !== testCase.expectLogLevel) { + continue; + } + // Check if all substrings are included + if (testCase.expectIncludes.some(str => !message.includes(str))) { + continue; + } + ok(true, testCase.description); + console_messages.splice(i, 1); + break; + } + } +} + +add_task(async function () { + requestLongerTimeout(4); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + Services.console.registerListener(on_new_console_messages); + + let promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await BrowserTestUtils.startLoadingURIString( + gBrowser.selectedBrowser, + `${TEST_PATH_HTTP}file_httpsonly_speculative_connect.html` + ); + await promiseLoaded; + + await BrowserTestUtils.waitForCondition(() => console_messages.length === 0); + + Services.console.unregisterListener(on_new_console_messages); +}); diff --git a/dom/security/test/https-only/browser_iframe_test.js b/dom/security/test/https-only/browser_iframe_test.js new file mode 100644 index 0000000000..eb4c1a97c3 --- /dev/null +++ b/dom/security/test/https-only/browser_iframe_test.js @@ -0,0 +1,223 @@ +// Bug 1658264 - Https-Only: HTTPS-Only and iFrames +// https://bugzilla.mozilla.org/show_bug.cgi?id=1658264 +"use strict"; + +// > How does this test work? +// We're sending a request to file_iframe_test.sjs with various +// browser-configurations. The sjs-file returns a website with two iFrames +// loading the same sjs-file again. One iFrame is same origin (example.com) and +// the other cross-origin (example.org) Each request gets saved in a semicolon +// seperated list of strings. The sjs-file gets initialized with the +// query-string "setup" and the result string can be polled with "results". Each +// string has this format: {top/com/org}-{queryString}-{scheme}. In the end +// we're just checking if all expected requests were recorded and had the +// correct scheme. Requests that are meant to fail should explicitly not be +// contained in the list of results. + +// The test loads all tabs and evaluates when all have finished loading +// it may take quite a long time. +// This requires more twice as much as the default 45 seconds per test: +requestLongerTimeout(2); +SimpleTest.requestCompleteLog(); + +add_task(async function () { + await setup(); + + // Using this variable to parallelize and collect tests + let testSet = []; + + /* + * HTTPS-Only Mode disabled + */ + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", false]], + }); + + // Top-Level scheme: HTTP + // NOTE(freddyb): Test case temporarily disabled. See bug 1735565 + /*testSet.push( + runTest({ + queryString: "test1.1", + topLevelScheme: "http", + + expectedTopLevel: "http", + expectedSameOrigin: "http", + expectedCrossOrigin: "http", + }) + );*/ + // Top-Level scheme: HTTPS + testSet.push( + runTest({ + queryString: "test1.2", + topLevelScheme: "https", + + expectedTopLevel: "https", + expectedSameOrigin: "fail", + expectedCrossOrigin: "fail", + }) + ); + + await Promise.all(testSet); + testSet = []; + /* + * HTTPS-Only Mode enabled, no exception + */ + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + // Top-Level scheme: HTTP + testSet.push( + runTest({ + queryString: "test2.1", + topLevelScheme: "http", + + expectedTopLevel: "https", + expectedSameOrigin: "https", + expectedCrossOrigin: "https", + }) + ); + // Top-Level scheme: HTTPS + testSet.push( + runTest({ + queryString: "test2.2", + topLevelScheme: "https", + + expectedTopLevel: "https", + expectedSameOrigin: "https", + expectedCrossOrigin: "https", + }) + ); + + await Promise.all(testSet); + testSet = []; + + /* + * HTTPS-Only enabled, with exceptions + * for http://example.org and http://example.com + */ + // Exempting example.org (cross-site) should not affect anything + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://example.org", + }, + ]); + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://example.com", + }, + ]); + + // Top-Level scheme: HTTP + await runTest({ + queryString: "test3.1", + topLevelScheme: "http", + + expectedTopLevel: "http", + expectedSameOrigin: "http", + expectedCrossOrigin: "http", + }); + + await SpecialPowers.popPermissions(); + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "https://example.com", + }, + ]); + // Top-Level scheme: HTTPS + await runTest({ + queryString: "test3.2", + topLevelScheme: "https", + + expectedTopLevel: "https", + expectedSameOrigin: "fail", + expectedCrossOrigin: "fail", + }); + + // Remove permissions again (has to be done manually for some reason?) + await SpecialPowers.popPermissions(); + await SpecialPowers.popPermissions(); + + await evaluate(); +}); + +const SERVER_URL = scheme => + `${scheme}://example.com/browser/dom/security/test/https-only/file_iframe_test.sjs?`; +let shouldContain = []; +let shouldNotContain = []; + +async function setup() { + info(`TEST-CASE-setup - A`); + const response = await fetch(SERVER_URL("https") + "setup"); + info(`TEST-CASE-setup - B`); + const txt = await response.text(); + info(`TEST-CASE-setup - C`); + if (txt != "ok") { + ok(false, "Failed to setup test server."); + finish(); + } +} + +async function evaluate() { + info(`TEST-CASE-evaluate - A`); + const response = await fetch(SERVER_URL("https") + "results"); + info(`TEST-CASE-evaluate - B`); + const requestResults = (await response.text()).split(";"); + info(`TEST-CASE-evaluate - C`); + + shouldContain.map(str => + ok(requestResults.includes(str), `Results should contain '${str}'.`) + ); + shouldNotContain.map(str => + ok(!requestResults.includes(str), `Results shouldn't contain '${str}'.`) + ); +} + +async function runTest(test) { + const queryString = test.queryString; + info(`TEST-CASE-${test.queryString} - runTest BEGIN`); + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + let loaded = BrowserTestUtils.browserLoaded( + browser, + false, // includeSubFrames + SERVER_URL(test.expectedTopLevel) + queryString, + false // maybeErrorPage + ); + BrowserTestUtils.startLoadingURIString( + browser, + SERVER_URL(test.topLevelScheme) + queryString + ); + info(`TEST-CASE-${test.queryString} - Before 'await loaded'`); + await loaded; + info(`TEST-CASE-${test.queryString} - After 'await loaded'`); + }); + info(`TEST-CASE-${test.queryString} - After 'await withNewTab'`); + + if (test.expectedTopLevel !== "fail") { + shouldContain.push(`top-${queryString}-${test.expectedTopLevel}`); + } else { + shouldNotContain.push(`top-${queryString}-http`); + shouldNotContain.push(`top-${queryString}-https`); + } + + if (test.expectedSameOrigin !== "fail") { + shouldContain.push(`com-${queryString}-${test.expectedSameOrigin}`); + } else { + shouldNotContain.push(`com-${queryString}-http`); + shouldNotContain.push(`com-${queryString}-https`); + } + + if (test.expectedCrossOrigin !== "fail") { + shouldContain.push(`org-${queryString}-${test.expectedCrossOrigin}`); + } else { + shouldNotContain.push(`org-${queryString}-http`); + shouldNotContain.push(`org-${queryString}-https`); + } + info(`TEST-CASE-${test.queryString} - runTest END`); +} diff --git a/dom/security/test/https-only/browser_navigation.js b/dom/security/test/https-only/browser_navigation.js new file mode 100644 index 0000000000..8c4609a57a --- /dev/null +++ b/dom/security/test/https-only/browser_navigation.js @@ -0,0 +1,94 @@ +"use strict"; + +// For each FIRST_URL_* this test does the following: +// 1. Navigate to FIRST_URL_* +// 2. Check if we are on a HTTPS-Only error page +// 3. Navigate to SECOND_URL +// 4. Navigate back +// 5. Check if we are on a HTTPS-Only error page + +const FIRST_URL_SECURE = "https://example.com"; +const FIRST_URL_INSECURE_REDIRECT = + "http://example.com/browser/dom/security/test/https-only/file_redirect_to_insecure.sjs"; +const FIRST_URL_INSECURE_NOCERT = "http://nocert.example.com"; +const SECOND_URL = "https://example.org"; + +function waitForPage() { + return new Promise(resolve => { + BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser).then(resolve); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(resolve); + }); +} + +async function verifyErrorPage(expectErrorPage = true) { + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [expectErrorPage], + async function (_expectErrorPage) { + let doc = content.document; + let innerHTML = doc.body.innerHTML; + let errorPageL10nId = "about-httpsonly-title-alert"; + + is( + innerHTML.includes(errorPageL10nId) && + doc.documentURI.startsWith("about:httpsonlyerror"), + _expectErrorPage, + "we should be on the https-only error page" + ); + } + ); +} + +async function runTest( + firstUrl, + expectErrorPageOnFirstVisit, + expectErrorPageOnSecondVisit +) { + let loaded = waitForPage(); + info("Loading first page"); + BrowserTestUtils.startLoadingURIString(gBrowser, firstUrl); + await loaded; + await verifyErrorPage(expectErrorPageOnFirstVisit); + + loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + info("Navigating to second page"); + await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [SECOND_URL], + async url => (content.location.href = url) + ); + await loaded; + + // Go back one site by clicking the back button + loaded = BrowserTestUtils.waitForLocationChange(gBrowser); + info("Clicking back button"); + let backButton = document.getElementById("back-button"); + backButton.click(); + await loaded; + await verifyErrorPage(expectErrorPageOnSecondVisit); +} + +add_task(async function () { + waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + // We don't expect any HTTPS-Only error pages, on the first and second visit of this URL, + // since the URL is reachable via https. + await runTest(FIRST_URL_SECURE, false, false); + + // Since trying to upgrade this url will result in being redirected again to the insecure + // site, we are not able to upgrade it and a HTTPS-Only error page is shown. + // This is happening both on the first and second visit. + await runTest(FIRST_URL_INSECURE_REDIRECT, true, true); + + // Similar to the previous case, we can not upgrade this URL, since this time it has a + // invalid certificate. We would expect a HTTPS-Only error page on both vists, but it is only + // shown on the first one, on the second one we get an errror page about the invalid + // certificate instead (Bug 1848117). + await runTest(FIRST_URL_INSECURE_NOCERT, true, false); + + finish(); +}); diff --git a/dom/security/test/https-only/browser_redirect_tainting.js b/dom/security/test/https-only/browser_redirect_tainting.js new file mode 100644 index 0000000000..0823ec4658 --- /dev/null +++ b/dom/security/test/https-only/browser_redirect_tainting.js @@ -0,0 +1,39 @@ +/* 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/. */ + +// Test steps: +// 1. Load file_redirect_tainting.sjs?html. +// 2. The server returns an html which loads an image at http://example.net. +// 3. The image request will be upgraded to HTTPS since HTTPS-only mode is on. +// 4. In file_redirect_tainting.sjs, we set "Access-Control-Allow-Origin" to +// the value of the Origin header. +// 5. If the vlaue does not match, the image won't be loaded. +async function do_test() { + let requestUrl = `https://example.com/browser/dom/security/test/https-only/file_redirect_tainting.sjs?html`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: requestUrl, + waitForLoad: true, + }, + async function (browser) { + let imageLoaded = await SpecialPowers.spawn(browser, [], function () { + let image = content.document.getElementById("test_image"); + return image && image.complete && image.naturalHeight !== 0; + }); + await Assert.ok(imageLoaded, "test_image should be loaded"); + } + ); +} + +add_task(async function test_https_only_redirect_tainting() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + await do_test(); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/dom/security/test/https-only/browser_save_as.js b/dom/security/test/https-only/browser_save_as.js new file mode 100644 index 0000000000..309dd69c79 --- /dev/null +++ b/dom/security/test/https-only/browser_save_as.js @@ -0,0 +1,187 @@ +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", + this +); + +// Using insecure HTTP URL for a test cases around HTTP/HTTPS download interaction +// eslint-disable-next-line @microsoft/sdl/no-insecure-url +const HTTP_LINK = `http://example.org/`; +const HTTPS_LINK = `https://example.org/`; +const TEST_PATH = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com/browser/dom/security/test/https-only/file_save_as.html"; + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +const tempDir = createTemporarySaveDirectory(); +MockFilePicker.displayDirectory = tempDir; + +add_setup(async function () { + info("Setting MockFilePicker."); + mockTransferRegisterer.register(); + + registerCleanupFunction(function () { + mockTransferRegisterer.unregister(); + MockFilePicker.cleanup(); + tempDir.remove(true); + }); +}); + +function createTemporarySaveDirectory() { + let saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + saveDir.append("testsavedir"); + saveDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + return saveDir; +} + +function createPromiseForObservingChannel(expectedUrl) { + return new Promise(resolve => { + let observer = (aSubject, aTopic) => { + if (aTopic === "http-on-modify-request") { + let httpChannel = aSubject.QueryInterface(Ci.nsIHttpChannel); + + if (httpChannel.URI.spec != expectedUrl) { + return; + } + + Services.obs.removeObserver(observer, "http-on-modify-request"); + resolve(); + } + }; + + Services.obs.addObserver(observer, "http-on-modify-request"); + }); +} + +function createPromiseForTransferComplete() { + return new Promise(resolve => { + MockFilePicker.showCallback = fp => { + info("MockFilePicker showCallback"); + + let fileName = fp.defaultString; + let destFile = tempDir.clone(); + destFile.append(fileName); + + MockFilePicker.setFiles([destFile]); + MockFilePicker.filterIndex = 0; // kSaveAsType_Complete + + MockFilePicker.showCallback = null; + mockTransferCallback = function (downloadSuccess) { + ok(downloadSuccess, "File should have been downloaded successfully"); + mockTransferCallback = () => {}; + resolve(); + }; + }; + }); +} + +function createPromiseForConsoleError(message) { + return new Promise((resolve, reject) => { + function listener(msgObj) { + let text = msgObj.message; + if (text.includes(message)) { + info(`Found occurence of '${message}'`); + Services.console.unregisterListener(listener); + resolve(); + } + } + Services.console.registerListener(listener); + }); +} + +async function runTest(selector, expectedUrl, expectedError) { + info(`Open a new tab for testing "Save link as" in context menu.`); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PATH); + + let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + + let browser = gBrowser.selectedBrowser; + + info("Open the context menu."); + await BrowserTestUtils.synthesizeMouseAtCenter( + selector, + { + type: "contextmenu", + button: 2, + }, + browser + ); + + await popupShownPromise; + + let downloadEndPromise = expectedError + ? createPromiseForConsoleError(expectedError) + : createPromiseForTransferComplete(); + let observerPromise = createPromiseForObservingChannel(expectedUrl); + + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + + // Select "Save As" option from context menu. + let saveElement = document.getElementById(`context-savelink`); + info("Triggering the save process."); + contextMenu.activateItem(saveElement); + + info("Waiting for the channel."); + await observerPromise; + + info( + expectedError + ? "Waiting for error in console." + : "Wait until the save is finished." + ); + await downloadEndPromise; + + info("Wait until the menu is closed."); + await popupHiddenPromise; + + BrowserTestUtils.removeTab(tab); +} + +async function setHttpsFirstAndOnlyPrefs(httpsFirst, httpsOnly) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_first", httpsFirst], + ["dom.security.https_only_mode", httpsOnly], + ], + }); +} + +add_task(async function testBaseline() { + // Run with HTTPS-First and HTTPS-Only disabled + await setHttpsFirstAndOnlyPrefs(false, false); + await runTest("#insecure-link", HTTP_LINK, undefined); + await runTest("#secure-link", HTTPS_LINK, undefined); +}); + +add_task(async function testHttpsFirst() { + // Run with HTTPS-First enabled + // The the user will get a warning about really wanting to download + // from a insecure site, because we upgraded the top level document, + // but the download is still insecure. In the future we also want to + // upgrade these Save-As downloads. + await setHttpsFirstAndOnlyPrefs(true, false); + await runTest( + "#insecure-link", + HTTP_LINK, + "Blocked downloading insecure content “http://example.org/”." + ); + await runTest("#secure-link", HTTPS_LINK, undefined); +}); + +add_task(async function testHttpsOnly() { + // Run with HTTPS-Only enabled + // Should have same behaviour as HTTPS-First + await setHttpsFirstAndOnlyPrefs(false, true); + await runTest( + "#insecure-link", + HTTP_LINK, + "Blocked downloading insecure content “http://example.org/”." + ); + await runTest("#secure-link", HTTPS_LINK, undefined); +}); diff --git a/dom/security/test/https-only/browser_triggering_principal_exemption.js b/dom/security/test/https-only/browser_triggering_principal_exemption.js new file mode 100644 index 0000000000..09a867a63b --- /dev/null +++ b/dom/security/test/https-only/browser_triggering_principal_exemption.js @@ -0,0 +1,72 @@ +// Bug 1662359 - Don't upgrade subresources whose triggering principal is exempt from HTTPS-Only mode. +// https://bugzilla.mozilla.org/bug/1662359 +"use strict"; + +const TRIGGERING_PAGE = "http://example.org"; +const LOADED_RESOURCE = "http://example.com"; + +add_task(async function () { + // Enable HTTPS-Only Mode + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + await runTest( + "Request with not exempt triggering principal should get upgraded.", + "https://" + ); + + // Now exempt the triggering page + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: TRIGGERING_PAGE, + }, + ]); + + await runTest( + "Request with exempt triggering principal should not get upgraded.", + "http://" + ); + + await SpecialPowers.popPermissions(); +}); + +async function runTest(desc, startsWith) { + const responseURL = await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", LOADED_RESOURCE); + + // Replace loadinfo with one whose triggeringPrincipal is a content + // principal for TRIGGERING_PAGE. + const triggeringPrincipal = + Services.scriptSecurityManager.createContentPrincipalFromOrigin( + TRIGGERING_PAGE + ); + let dummyURI = Services.io.newURI(LOADED_RESOURCE); + let dummyChannel = NetUtil.newChannel({ + uri: dummyURI, + triggeringPrincipal, + loadingPrincipal: xhr.channel.loadInfo.loadingPrincipal, + securityFlags: xhr.channel.loadInfo.securityFlags, + contentPolicyType: xhr.channel.loadInfo.externalContentPolicyType, + }); + xhr.channel.loadInfo = dummyChannel.loadInfo; + + xhr.onreadystatechange = () => { + // We don't care about the result, just if Firefox upgraded the URL + // internally. + if ( + xhr.readyState !== XMLHttpRequest.OPENED || + xhr.readyState !== XMLHttpRequest.UNSENT + ) { + // Let's make sure this function doesn't get called anymore + xhr.onreadystatechange = undefined; + resolve(xhr.responseURL); + } + }; + xhr.send(); + }); + ok(responseURL.startsWith(startsWith), desc); +} diff --git a/dom/security/test/https-only/browser_upgrade_exceptions.js b/dom/security/test/https-only/browser_upgrade_exceptions.js new file mode 100644 index 0000000000..8611b32a0f --- /dev/null +++ b/dom/security/test/https-only/browser_upgrade_exceptions.js @@ -0,0 +1,86 @@ +// Bug 1625448 - HTTPS Only Mode - Exceptions for loopback and local IP addresses +// https://bugzilla.mozilla.org/show_bug.cgi?id=1631384 +// This test ensures that various configurable upgrade exceptions work +"use strict"; + +add_task(async function () { + requestLongerTimeout(2); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + // Loopback test + await runTest( + "Loopback IP addresses should always be exempt from upgrades (localhost)", + "http://localhost", + "http://" + ); + await runTest( + "Loopback IP addresses should always be exempt from upgrades (127.0.0.1)", + "http://127.0.0.1", + "http://" + ); + // Default local-IP and onion tests + await runTest( + "Local IP addresses should be exempt from upgrades by default", + "http://10.0.250.250", + "http://" + ); + await runTest( + "Hosts ending with .onion should be be exempt from HTTPS-Only upgrades by default", + "http://grocery.shopping.for.one.onion", + "http://" + ); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode.upgrade_local", true], + ["dom.security.https_only_mode.upgrade_onion", true], + ], + }); + + // Local-IP and onion tests with upgrade enabled + await runTest( + "Local IP addresses should get upgraded when 'dom.security.https_only_mode.upgrade_local' is set to true", + "http://10.0.250.250", + "https://" + ); + await runTest( + "Hosts ending with .onion should get upgraded when 'dom.security.https_only_mode.upgrade_onion' is set to true", + "http://grocery.shopping.for.one.onion", + "https://" + ); + // Local-IP request with HTTPS_ONLY_EXEMPT flag + await runTest( + "The HTTPS_ONLY_EXEMPT flag should overrule upgrade-prefs", + "http://10.0.250.250", + "http://", + true + ); +}); + +async function runTest(desc, url, startsWith, exempt = false) { + const responseURL = await new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.timeout = 1200; + xhr.open("GET", url); + if (exempt) { + xhr.channel.loadInfo.httpsOnlyStatus |= Ci.nsILoadInfo.HTTPS_ONLY_EXEMPT; + } + xhr.onreadystatechange = () => { + // We don't care about the result and it's possible that + // the requests might even succeed in some testing environments + if ( + xhr.readyState !== XMLHttpRequest.OPENED || + xhr.readyState !== XMLHttpRequest.UNSENT + ) { + // Let's make sure this function doesn't get caled anymore + xhr.onreadystatechange = undefined; + resolve(xhr.responseURL); + } + }; + xhr.send(); + }); + ok(responseURL.startsWith(startsWith), desc); +} diff --git a/dom/security/test/https-only/browser_upgrade_exemption.js b/dom/security/test/https-only/browser_upgrade_exemption.js new file mode 100644 index 0000000000..23d857b511 --- /dev/null +++ b/dom/security/test/https-only/browser_upgrade_exemption.js @@ -0,0 +1,80 @@ +"use strict"; + +const PAGE_WITHOUT_SCHEME = "://example.com"; + +add_task(async function () { + // Load a insecure page with HTTPS-Only and HTTPS-First disabled + await runTest({ + loadScheme: "http", + expectScheme: "http", + }); + + // Load a secure page with HTTPS-Only and HTTPS-First disabled + await runTest({ + loadScheme: "https", + expectScheme: "https", + }); + + // Load a exempted insecure page with HTTPS-Only and HTTPS-First disabled + await runTest({ + exempt: true, + loadScheme: "http", + expectScheme: "http", + }); + + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + // Load a insecure page with HTTPS-Only enabled + await runTest({ + loadScheme: "http", + expectScheme: "https", + }); + + // Load a exempted insecure page with HTTPS-Only enabled + await runTest({ + exempt: true, + loadScheme: "http", + expectScheme: "http", + }); + + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_first", true]], + }); + + // Load a insecure page with HTTPS-First enabled + await runTest({ + loadScheme: "http", + expectScheme: "https", + }); + + // Load a exempted insecure page with HTTPS-First enabled + await runTest({ + exempt: true, + loadScheme: "http", + expectScheme: "http", + }); +}); + +async function runTest(options) { + const { exempt = false, loadScheme, expectScheme } = options; + const page = loadScheme + PAGE_WITHOUT_SCHEME; + + if (exempt) { + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: page, + }, + ]); + } + + await BrowserTestUtils.withNewTab(page, async function (browser) { + is(browser.currentURI.scheme, expectScheme, "Unexpected scheme"); + await SpecialPowers.popPermissions(); + await SpecialPowers.popPrefEnv(); + }); +} diff --git a/dom/security/test/https-only/browser_user_gesture.js b/dom/security/test/https-only/browser_user_gesture.js new file mode 100644 index 0000000000..e7d6a20318 --- /dev/null +++ b/dom/security/test/https-only/browser_user_gesture.js @@ -0,0 +1,55 @@ +// Bug 1725026 - HTTPS Only Mode - Test if a load triggered by a user gesture +// https://bugzilla.mozilla.org/show_bug.cgi?id=1725026 +// Test if a load triggered by a user gesture can be upgraded to HTTPS +// successfully. + +"use strict"; + +const testPathUpgradeable = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://example.com" +); + +const kTestURI = testPathUpgradeable + "file_user_gesture.html"; + +add_task(async function () { + // Enable HTTPS-Only Mode and register console-listener + await SpecialPowers.pushPrefEnv({ + set: [["dom.security.https_only_mode", true]], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + const loaded = BrowserTestUtils.browserLoaded(browser, false, null, true); + // 1. Upgrade a page to https:// + BrowserTestUtils.startLoadingURIString(browser, kTestURI); + await loaded; + await ContentTask.spawn(browser, {}, async args => { + ok( + content.document.location.href.startsWith("https://"), + "Should be https" + ); + + // 2. Trigger a load by clicking button. + // The scheme of the link url is `http` and the load should be able to + // upgraded to `https` because of HTTPS-only mode. + let button = content.document.getElementById("httpLinkButton"); + await EventUtils.synthesizeMouseAtCenter( + button, + { type: "mousedown" }, + content + ); + await EventUtils.synthesizeMouseAtCenter( + button, + { type: "mouseup" }, + content + ); + await ContentTaskUtils.waitForCondition(() => { + return content.document.location.href.startsWith("https://"); + }); + ok( + content.document.location.href.startsWith("https://"), + "Should be https" + ); + }); + }); +}); diff --git a/dom/security/test/https-only/browser_websocket_exceptions.js b/dom/security/test/https-only/browser_websocket_exceptions.js new file mode 100644 index 0000000000..a6e5336c63 --- /dev/null +++ b/dom/security/test/https-only/browser_websocket_exceptions.js @@ -0,0 +1,68 @@ +"use strict"; + +const TEST_PATH_HTTP = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://localhost:9898" +); + +let WEBSOCKET_DOC_URL = `${TEST_PATH_HTTP}file_websocket_exceptions.html`; + +add_task(async function () { + // Here is a sequence of how this test works: + // 1. Dynamically inject a localhost iframe + // 2. Add an exemption for localhost + // 3. Fire up Websocket + // Generally local IP addresses are exempt from https-only, but if we do not add + // an exemption for localhost, then the TriggeringPrincipal of the WebSocket is + // `not` exempt and we would upgrade ws to wss. + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.security.https_only_mode", true], + ["network.proxy.allow_hijacking_localhost", true], + ], + }); + + await BrowserTestUtils.withNewTab("about:blank", async function (browser) { + let loaded = BrowserTestUtils.browserLoaded(browser); + + BrowserTestUtils.startLoadingURIString(browser, WEBSOCKET_DOC_URL); + await loaded; + + await SpecialPowers.spawn(browser, [], async function () { + // Part 1: + let myIframe = content.document.createElement("iframe"); + content.document.body.appendChild(myIframe); + myIframe.src = + "http://localhost:9898/browser/dom/security/test/https-only/file_websocket_exceptions_iframe.html"; + + myIframe.onload = async function () { + // Part 2: + await SpecialPowers.pushPermissions([ + { + type: "https-only-load-insecure", + allow: true, + context: "http://localhost:9898", + }, + ]); + // Part 3. + myIframe.contentWindow.postMessage({ myMessage: "runWebSocket" }, "*"); + }; + + const promise = new Promise(resolve => { + content.addEventListener("WebSocketEnded", resolve, { + once: true, + }); + }); + + const { detail } = await promise; + + is(detail.state, "onopen", "sanity: websocket loaded"); + ok( + detail.url.startsWith("ws://example.com/tests"), + "exempt websocket should not be upgraded to wss://" + ); + }); + }); + await SpecialPowers.popPermissions(); +}); diff --git a/dom/security/test/https-only/file_background_redirect.sjs b/dom/security/test/https-only/file_background_redirect.sjs new file mode 100644 index 0000000000..45e01797e3 --- /dev/null +++ b/dom/security/test/https-only/file_background_redirect.sjs @@ -0,0 +1,32 @@ +// Custom *.sjs file specifically for the needs of Bug 1683015 +"use strict"; + +let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +async function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + let query = request.queryString; + if (request.scheme === "https" && query === "start") { + // Simulating long repsonse time by processing the https request + // using a 5 seconds delay. Note that the http background request + // gets send after 3 seconds + response.processAsync(); + setTimeout(() => { + response.setStatusLine("1.1", 200, "OK"); + response.write( + "<html><body>Test Page for Bug 1683015 loaded</body></html>" + ); + response.finish(); + }, 5000); /* wait 5 seconds */ + return; + } + + // we should never get here, but just in case return something unexpected + response.setStatusLine("1.1", 404, "Not Found"); + response.write("<html><body>SHOULDN'T DISPLAY THIS PAGE</body></html>"); +} diff --git a/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs b/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs new file mode 100644 index 0000000000..a8a9083ef3 --- /dev/null +++ b/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs @@ -0,0 +1,67 @@ +// Custom *.sjs file specifically for the needs of Bug 1691888 +"use strict"; + +const REDIRECT_META = ` + <html> + <head> + <meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test1b'"> + </head> + <body> + META REDIRECT + </body> + </html>`; + +const REDIRECT_JS = ` + <html> + <body> + JS REDIRECT + <script> + let url= "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test2b"; + window.location = url; + </script> + </body> + </html>`; + +const REDIRECT_302 = + "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test3b"; + +const REDIRECT_302_DIFFERENT_PATH = + "http://example.com/tests/dom/security/test/https-only/file_user_gesture.html"; + +function handleRequest(request, response) { + // avoid confusing cache behaviour + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + + // if the scheme is not https, meaning that the initial request did not + // get upgraded, then we rather fall through and display unexpected content. + if (request.scheme === "https") { + let query = request.queryString; + + if (query === "test1a") { + response.write(REDIRECT_META); + return; + } + + if (query === "test2a") { + response.write(REDIRECT_JS); + return; + } + + if (query === "test3a") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", REDIRECT_302, false); + return; + } + + if (query === "test4a") { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", REDIRECT_302_DIFFERENT_PATH, false); + return; + } + } + + // we should never get here, just in case, + // let's return something unexpected + response.write("<html><body>DO NOT DISPLAY THIS</body></html>"); +} diff --git a/dom/security/test/https-only/file_bug1874801.html b/dom/security/test/https-only/file_bug1874801.html new file mode 100644 index 0000000000..58c2f03c81 --- /dev/null +++ b/dom/security/test/https-only/file_bug1874801.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Bug 1874801</title> +</head> +<body> + <img src="http://example.com/browser/dom/security/test/https-only/file_bug1874801.sjs"> +</body> +</html> diff --git a/dom/security/test/https-only/file_bug1874801.sjs b/dom/security/test/https-only/file_bug1874801.sjs new file mode 100644 index 0000000000..ce84af1d5f --- /dev/null +++ b/dom/security/test/https-only/file_bug1874801.sjs @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + if (request.scheme === "https") { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/svg+xml"); + response.write( + `<svg version="1.1" width="100" height="40" xmlns="http://www.w3.org/2000/svg"><text x="20" y="20">HTTPS</text></svg>` + ); + return; + } + if (request.scheme === "http") { + response.setStatusLine(request.httpVersion, 400, "Bad Request"); + } +} diff --git a/dom/security/test/https-only/file_console_logging.html b/dom/security/test/https-only/file_console_logging.html new file mode 100644 index 0000000000..94e07cddd9 --- /dev/null +++ b/dom/security/test/https-only/file_console_logging.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1625448 - HTTPS Only Mode - Tests for console logging</title> +</head> +<body> + <!-- These files don't exist since we only care if the webserver can respond. --> + <!-- This request can get upgraded. --> + <img src="http://example.com/file_1.jpg"> + <!-- This request can't get upgraded --> + <img src="http://self-signed.example.com/file_2.jpg"> + + <iframe src="http://self-signed.example.com/browser/dom/security/test/https-only/file_console_logging.html"></iframe> +</body> +</html> diff --git a/dom/security/test/https-only/file_cors_mixedcontent.html b/dom/security/test/https-only/file_cors_mixedcontent.html new file mode 100644 index 0000000000..50d32954ef --- /dev/null +++ b/dom/security/test/https-only/file_cors_mixedcontent.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + + <script> + addEventListener("StartFetch", async function() { + let comResult; + let orgResult; + + await Promise.all([ + fetch("http://example.com/") + .then(() => { + comResult = "success"; + }) + .catch(() => { + comResult = "error"; + }), + fetch("http://example.org/") + .then(() => { + orgResult = "success"; + }) + .catch(() => { + orgResult = "error"; + }), + ]); + + window.dispatchEvent(new CustomEvent("FetchEnded", { + detail: { comResult, orgResult } + })); + + }) + </script> +</head> + +<body> + <h2>Https-Only: CORS and MixedContent tests</h2> + <p><a href="https://bugzilla.mozilla.org/bug/1659505">Bug 1659505</a></p> +</body> + +</html> diff --git a/dom/security/test/https-only/file_fragment.html b/dom/security/test/https-only/file_fragment.html new file mode 100644 index 0000000000..49cff76f9a --- /dev/null +++ b/dom/security/test/https-only/file_fragment.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<script> +// sends url of current window and onbeforeunloadMessage +// when we enter here test failed. +function beforeunload(){ + window.opener.postMessage({ + info: "before-unload", + result: window.location.hash, + button: false, + }, "*"); +} + +// sends url of current window and then click on button +window.onload = function (){ + // get button by ID + let button = window.document.getElementById("clickMeButton"); + // if getting button was successful, buttonExist = true + let buttonExist = button !== null; + // send loading message + window.opener.postMessage({ + info: "onload", + result: window.location.href, + button: buttonExist, + }, "*"); + // click button + button.click(); +} +// after button clicked and paged scrolled sends URL of current window +window.onscroll = function(){ + window.opener.postMessage({ + info: "scrolled-to-foo", + result: window.location.href, + button: true, + documentURI: document.documentURI, + }, "*"); + } + + +</script> +<body onbeforeunload="/*just to notify if we load a new page*/ beforeunload()";> + <a id="clickMeButton" href="http://example.com/tests/dom/security/test/https-only/file_fragment.html#foo">Click me</a> + <div style="height: 1000px; border: 1px solid black;"> space</div> + <a name="foo" href="http://example.com/tests/dom/security/test/https-only/file_fragment.html">foo</a> + <div style="height: 1000px; border: 1px solid black;">space</div> +</body> +</html> diff --git a/dom/security/test/https-only/file_fragment_noscript.html b/dom/security/test/https-only/file_fragment_noscript.html new file mode 100644 index 0000000000..609961fd98 --- /dev/null +++ b/dom/security/test/https-only/file_fragment_noscript.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<body> + <a id="clickMeButton" href="http://example.com/browser/dom/security/test/https-only/file_fragment_noscript.html#foo">Click me</a> + <div style="height: 1000px; border: 1px solid black;"> space</div> + <a name="foo" href="http://example.com/browser/dom/security/test/https-only/file_fragment_noscript.html">foo</a> + <div style="height: 1000px; border: 1px solid black;">space</div> +</body> +</html> diff --git a/dom/security/test/https-only/file_http_background_auth_request.sjs b/dom/security/test/https-only/file_http_background_auth_request.sjs new file mode 100644 index 0000000000..80fc8ffcf7 --- /dev/null +++ b/dom/security/test/https-only/file_http_background_auth_request.sjs @@ -0,0 +1,16 @@ +// Custom *.sjs file specifically for the needs of Bug 1665062 + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.scheme === "https") { + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setStatusLine(request.httpVersion, 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", 'Basic realm="bug1665062"'); + return; + } + + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/https-only/file_http_background_request.sjs b/dom/security/test/https-only/file_http_background_request.sjs new file mode 100644 index 0000000000..ef0e2ce5bd --- /dev/null +++ b/dom/security/test/https-only/file_http_background_request.sjs @@ -0,0 +1,15 @@ +// Custom *.sjs file specifically for the needs of Bug 1663396 + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + + if (request.scheme === "https") { + // Simulating a timeout by processing the https request + // async and *never* return anything! + response.processAsync(); + return; + } + // we should never get here; just in case, return something unexpected + response.write("do'h"); +} diff --git a/dom/security/test/https-only/file_httpsonly_speculative_connect.html b/dom/security/test/https-only/file_httpsonly_speculative_connect.html new file mode 100644 index 0000000000..46a10401f9 --- /dev/null +++ b/dom/security/test/https-only/file_httpsonly_speculative_connect.html @@ -0,0 +1 @@ +<html><body>dummy file for speculative https-only upgrade test</body></html> diff --git a/dom/security/test/https-only/file_iframe_test.sjs b/dom/security/test/https-only/file_iframe_test.sjs new file mode 100644 index 0000000000..611870c87c --- /dev/null +++ b/dom/security/test/https-only/file_iframe_test.sjs @@ -0,0 +1,58 @@ +// Bug 1658264 - HTTPS-Only and iFrames +// see browser_iframe_test.js + +const IFRAME_CONTENT = ` +<!DOCTYPE HTML> +<html> + <head><meta charset="utf-8"></head> + <body>Helo Friend!</body> +</html>`; +const DOCUMENT_CONTENT = q => ` +<!DOCTYPE HTML> +<html> + <head><meta charset="utf-8"></head> + <body> + <iframe src="http://example.com/browser/dom/security/test/https-only/file_iframe_test.sjs?com-${q}"></iframe> + <iframe src="http://example.org/browser/dom/security/test/https-only/file_iframe_test.sjs?org-${q}"></iframe> + </body> +</html>`; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + let queryString = request.queryString; + const queryScheme = request.scheme; + + // Setup the state with an empty string and return "ok" + if (queryString == "setup") { + setState("receivedQueries", ""); + response.write("ok"); + return; + } + + let receivedQueries = getState("receivedQueries"); + + // Return result-string + if (queryString == "results") { + response.write(receivedQueries); + return; + } + + // Add semicolon to seperate strings + if (receivedQueries !== "") { + receivedQueries += ";"; + } + + // Requests from iFrames start with com or org + if (queryString.startsWith("com-") || queryString.startsWith("org-")) { + receivedQueries += queryString; + setState("receivedQueries", `${receivedQueries}-${queryScheme}`); + response.write(IFRAME_CONTENT); + return; + } + + // Everything else has to be a top-level request + receivedQueries += `top-${queryString}`; + setState("receivedQueries", `${receivedQueries}-${queryScheme}`); + response.write(DOCUMENT_CONTENT(queryString)); +} diff --git a/dom/security/test/https-only/file_insecure_reload.sjs b/dom/security/test/https-only/file_insecure_reload.sjs new file mode 100644 index 0000000000..a48ec4e20f --- /dev/null +++ b/dom/security/test/https-only/file_insecure_reload.sjs @@ -0,0 +1,27 @@ +// https://bugzilla.mozilla.org/show_bug.cgi?id=1702001 + +// An onload postmessage to window opener +const ON_LOAD = ` + <html> + <body> + send onload message... + <script type="application/javascript"> + window.opener.postMessage({result: 'you entered the http page', historyLength: history.length}, '*'); + </script> + </body> + </html>`; + +// When an https request is sent, cause a timeout so that the https-only error +// page is displayed. +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + if (request.scheme === "https") { + // Simulating a timeout by processing the https request + // async and *never* return anything! + response.processAsync(); + return; + } + if (request.scheme === "http") { + response.write(ON_LOAD); + } +} diff --git a/dom/security/test/https-only/file_redirect.sjs b/dom/security/test/https-only/file_redirect.sjs new file mode 100644 index 0000000000..c66cbaa226 --- /dev/null +++ b/dom/security/test/https-only/file_redirect.sjs @@ -0,0 +1,37 @@ +// https://bugzilla.mozilla.org/show_bug.cgi?id=1613063 + +// Step 1. Send request with redirect queryString (eg. file_redirect.sjs?302) +// Step 2. Server responds with corresponding redirect code to http://example.com/../file_redirect.sjs?check +// Step 3. Response from ?check indicates whether the redirected request was secure or not. + +const RESPONSE_SECURE = "secure-ok"; +const RESPONSE_INSECURE = "secure-error"; +const RESPONSE_ERROR = "unexpected-query"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + const query = request.queryString; + + // Send redirect header + if ((query >= 301 && query <= 303) || query == 307) { + const loc = + "http://example.com/tests/dom/security/test/https-only/file_redirect.sjs?check"; + response.setStatusLine(request.httpVersion, query, "Moved"); + response.setHeader("Location", loc, false); + return; + } + + // Check if scheme is http:// oder https:// + if (query == "check") { + const secure = + request.scheme == "https" ? RESPONSE_SECURE : RESPONSE_INSECURE; + response.setStatusLine(request.httpVersion, 200, "OK"); + response.write(secure); + return; + } + + // This should not happen + response.setStatusLine(request.httpVersion, 500, "OK"); + response.write(RESPONSE_ERROR); +} diff --git a/dom/security/test/https-only/file_redirect_tainting.sjs b/dom/security/test/https-only/file_redirect_tainting.sjs new file mode 100644 index 0000000000..9ecca88d6d --- /dev/null +++ b/dom/security/test/https-only/file_redirect_tainting.sjs @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +const body = `<!DOCTYPE html> +<html lang="en"> + <body> + <script> + let image = new Image(); + image.crossOrigin = "anonymous"; + image.src = "http://example.net/browser/dom/security/test/https-only/file_redirect_tainting.sjs?img"; + image.id = "test_image"; + document.body.appendChild(image); + </script> + </body> +</html>`; + +function handleRequest(request, response) { + if (request.queryString === "html") { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html"); + response.write(body); + return; + } + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "image/png"); + let origin = request.getHeader("Origin"); + response.setHeader("Access-Control-Allow-Origin", origin); + response.write(IMG_BYTES); +} diff --git a/dom/security/test/https-only/file_redirect_to_insecure.sjs b/dom/security/test/https-only/file_redirect_to_insecure.sjs new file mode 100644 index 0000000000..ea88223926 --- /dev/null +++ b/dom/security/test/https-only/file_redirect_to_insecure.sjs @@ -0,0 +1,16 @@ +// Redirect back to http if visited via https. This way we can simulate +// a site which can not be upgraded by HTTPS-Only. + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + if (request.scheme === "https") { + response.setStatusLine(request.httpVersion, "302", "Found"); + response.setHeader( + "Location", + // We explicitly want a insecure URL here, so disable eslint + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + `http://${request.host}${request.path}`, + false + ); + } +} diff --git a/dom/security/test/https-only/file_save_as.html b/dom/security/test/https-only/file_save_as.html new file mode 100644 index 0000000000..44232b16ec --- /dev/null +++ b/dom/security/test/https-only/file_save_as.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> +</head> +<body> + <a href="http://example.org" id="insecure-link">Insecure Link</a> + <a href="https://example.org" id="secure-link">Secure Link</a> +</body> +</html>
\ No newline at end of file diff --git a/dom/security/test/https-only/file_upgrade_insecure.html b/dom/security/test/https-only/file_upgrade_insecure.html new file mode 100644 index 0000000000..346cfbeb9c --- /dev/null +++ b/dom/security/test/https-only/file_upgrade_insecure.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1613063 - HTTPS Only Mode</title> + <!-- style --> + <link rel='stylesheet' type='text/css' href='http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?style' media='screen' /> + + <!-- font --> + <style> + @font-face { + font-family: "foofont"; + src: url('http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?font'); + } + .div_foo { font-family: "foofont"; } + </style> +</head> +<body> + + <!-- images: --> + <img src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?img"></img> + + <!-- redirects: upgrade http:// to https:// redirect to http:// and then upgrade to https:// again --> + <img src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?redirect-image"></img> + + <!-- script: --> + <script src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?script"></script> + + <!-- media: --> + <audio src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?media"></audio> + + <!-- objects: --> + <object width="10" height="10" data="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?object"></object> + + <!-- font: (apply font loaded in header to div) --> + <div class="div_foo">foo</div> + + <!-- iframe: (same origin) --> + <iframe src="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?iframe"> + <!-- within that iframe we load an image over http and make sure the requested gets upgraded to https --> + </iframe> + + <!-- xhr: --> + <script type="application/javascript"> + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?xhr"); + myXHR.send(null); + </script> + + <!-- websockets: upgrade ws:// to wss://--> + <script type="application/javascript"> + // WebSocket tests are not supported on Android yet. Bug 1566168 + const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + if (AppConstants.platform !== "android") { + var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/https-only/file_upgrade_insecure"); + mySocket.onopen = function(e) { + if (mySocket.url.includes("wss://")) { + window.parent.postMessage({result: "websocket-ok"}, "*"); + } + else { + window.parent.postMessage({result: "websocket-error"}, "*"); + } + mySocket.close(); + }; + mySocket.onerror = function(e) { + // debug information for Bug 1316305 + dump(" xxx mySocket.onerror: (mySocket): " + mySocket + "\n"); + dump(" xxx mySocket.onerror: (mySocket.url): " + mySocket.url + "\n"); + dump(" xxx mySocket.onerror: (e): " + e + "\n"); + dump(" xxx mySocket.onerror: (e.message): " + e.message + "\n"); + dump(" xxx mySocket.onerror: This might be related to Bug 1316305!\n"); + window.parent.postMessage({result: "websocket-unexpected-error"}, "*"); + }; + } + </script> + + <!-- form action: (upgrade POST from http:// to https://) --> + <iframe name='formFrame' id='formFrame'></iframe> + <form target="formFrame" action="http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?form" method="POST"> + <input name="foo" value="foo"> + <input type="submit" id="submitButton" formenctype='multipart/form-data' value="Submit form"> + </form> + <script type="text/javascript"> + var submitButton = document.getElementById('submitButton'); + submitButton.click(); + </script> + +</body> +</html> diff --git a/dom/security/test/https-only/file_upgrade_insecure_server.sjs b/dom/security/test/https-only/file_upgrade_insecure_server.sjs new file mode 100644 index 0000000000..7cf590141c --- /dev/null +++ b/dom/security/test/https-only/file_upgrade_insecure_server.sjs @@ -0,0 +1,112 @@ +// SJS file for HTTPS-Only Mode mochitests +// Bug 1613063 - HTTPS Only Mode + +const TOTAL_EXPECTED_REQUESTS = 11; + +const IFRAME_CONTENT = + "<!DOCTYPE HTML>" + + "<html>" + + "<head><meta charset='utf-8'>" + + "<title>Bug 1613063 - HTTPS Only Mode</title>" + + "</head>" + + "<body>" + + "<img src='http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?nested-img'></img>" + + "</body>" + + "</html>"; + +const expectedQueries = [ + "script", + "style", + "img", + "iframe", + "form", + "xhr", + "media", + "object", + "font", + "img-redir", + "nested-img", +]; + +function handleRequest(request, response) { + // avoid confusing cache behaviors + response.setHeader("Cache-Control", "no-cache", false); + var queryString = request.queryString; + + // initialize server variables and save the object state + // of the initial request, which returns async once the + // server has processed all requests. + if (queryString == "queryresult") { + setState("totaltests", TOTAL_EXPECTED_REQUESTS.toString()); + setState("receivedQueries", ""); + response.processAsync(); + setObjectState("queryResult", response); + return; + } + + // handle img redirect (https->http) + if (queryString == "redirect-image") { + var newLocation = + "http://example.com/tests/dom/security/test/https-only/file_upgrade_insecure_server.sjs?img-redir"; + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", newLocation, false); + return; + } + + // just in case error handling for unexpected queries + if (!expectedQueries.includes(queryString)) { + response.write("unexpected-response"); + return; + } + + // make sure all the requested queries are indeed https + queryString += request.scheme == "https" ? "-ok" : "-error"; + + var receivedQueries = getState("receivedQueries"); + + // images, scripts, etc. get queried twice, do not + // confuse the server by storing the preload as + // well as the actual load. If either the preload + // or the actual load is not https, then we would + // append "-error" in the array and the test would + // fail at the end. + if (receivedQueries.includes(queryString)) { + return; + } + + // append the result to the total query string array + if (receivedQueries != "") { + receivedQueries += ","; + } + receivedQueries += queryString; + setState("receivedQueries", receivedQueries); + + // keep track of how many more requests the server + // is expecting + var totaltests = parseInt(getState("totaltests")); + totaltests -= 1; + setState("totaltests", totaltests.toString()); + + // return content (img) for the nested iframe to test + // that subresource requests within nested contexts + // get upgraded as well. We also have to return + // the iframe context in case of an error so we + // can test both, using upgrade-insecure as well + // as the base case of not using upgrade-insecure. + if (queryString == "iframe-ok" || queryString == "iframe-error") { + response.write(IFRAME_CONTENT); + } + + // if we have received all the requests, we return + // the result back. + if (totaltests == 0) { + getObjectState("queryResult", function (queryResponse) { + if (!queryResponse) { + return; + } + receivedQueries = getState("receivedQueries"); + queryResponse.write(receivedQueries); + queryResponse.finish(); + }); + } +} diff --git a/dom/security/test/https-only/file_upgrade_insecure_wsh.py b/dom/security/test/https-only/file_upgrade_insecure_wsh.py new file mode 100644 index 0000000000..b7159c742b --- /dev/null +++ b/dom/security/test/https-only/file_upgrade_insecure_wsh.py @@ -0,0 +1,6 @@ +def web_socket_do_extra_handshake(request): + pass + + +def web_socket_transfer_data(request): + pass diff --git a/dom/security/test/https-only/file_user_gesture.html b/dom/security/test/https-only/file_user_gesture.html new file mode 100644 index 0000000000..ac67064bf0 --- /dev/null +++ b/dom/security/test/https-only/file_user_gesture.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Bug 1725026 - HTTPS Only Mode - Test if a load triggered by a user gesture can be upgraded to HTTPS</title> +</head> +<body> + <button id="httpLinkButton" onclick="location.href='http://example.com/tests/dom/security/test/https-only/file_console_logging.html'" type="button"> + button</button> +</body> +</html> diff --git a/dom/security/test/https-only/file_websocket_exceptions.html b/dom/security/test/https-only/file_websocket_exceptions.html new file mode 100644 index 0000000000..6c6ba07480 --- /dev/null +++ b/dom/security/test/https-only/file_websocket_exceptions.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + Dummy file which gets iframe injected<br/> +</body> +</html> diff --git a/dom/security/test/https-only/file_websocket_exceptions_iframe.html b/dom/security/test/https-only/file_websocket_exceptions_iframe.html new file mode 100644 index 0000000000..23c6af2d45 --- /dev/null +++ b/dom/security/test/https-only/file_websocket_exceptions_iframe.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script> + +window.addEventListener("message", receiveMessage); +function receiveMessage(event) { + window.removeEventListener("message", receiveMessage); + + var mySocket = new WebSocket("ws://example.com/tests/dom/security/test/https-only/file_upgrade_insecure"); + mySocket.onopen = function(e) { + parent.dispatchEvent(new CustomEvent("WebSocketEnded", { + detail: { url: mySocket.url, state: "onopen" } + })); + mySocket.close(); + }; + mySocket.onerror = function(e) { + parent.dispatchEvent(new CustomEvent("WebSocketEnded", { + detail: { url: mySocket.url, state: "onerror" } + })); + mySocket.close(); + }; +} +</script> +</head> +<body> + Https-Only: WebSocket exemption test in iframe</br> +</body> +</html> diff --git a/dom/security/test/https-only/hsts_headers.sjs b/dom/security/test/https-only/hsts_headers.sjs new file mode 100644 index 0000000000..72e82caaf3 --- /dev/null +++ b/dom/security/test/https-only/hsts_headers.sjs @@ -0,0 +1,24 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(request, response) { + if (request.queryString === "reset") { + // Reset the HSTS policy, prevent influencing other tests + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Strict-Transport-Security", "max-age=0"); + response.write("Resetting HSTS"); + return; + } + let hstsHeader = "max-age=60"; + response.setHeader("Strict-Transport-Security", hstsHeader); + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/html", false); + // Set header for csp upgrade + response.setHeader( + "Content-Security-Policy", + "upgrade-insecure-requests", + false + ); + response.setStatusLine(request.httpVersion, 200); + response.write("<!DOCTYPE html><html><body><h1>Ok!</h1></body></html>"); +} diff --git a/dom/security/test/https-only/mochitest.toml b/dom/security/test/https-only/mochitest.toml new file mode 100644 index 0000000000..e7ecd439c4 --- /dev/null +++ b/dom/security/test/https-only/mochitest.toml @@ -0,0 +1,65 @@ +[DEFAULT] +support-files = [ + "file_redirect.sjs", + "file_upgrade_insecure.html", + "file_upgrade_insecure_server.sjs", + "file_upgrade_insecure_wsh.py", +] +prefs = [ + "dom.security.https_first=false", + "security.mixed_content.upgrade_display_content=false", +] + +["test_break_endless_upgrade_downgrade_loop.html"] +skip-if = [ + "os == 'android'", # no support for error pages, Bug 1697866 + "http3", + "http2", +] +support-files = [ + "file_break_endless_upgrade_downgrade_loop.sjs", + "file_user_gesture.html", +] + +["test_fragment.html"] +support-files = ["file_fragment.html"] + +["test_http_background_auth_request.html"] +support-files = ["file_http_background_auth_request.sjs"] +skip-if = [ + "http3", + "http2", +] + +["test_http_background_request.html"] +support-files = ["file_http_background_request.sjs"] +skip-if = [ + "http3", + "http2", +] + +["test_insecure_reload.html"] +support-files = ["file_insecure_reload.sjs"] +skip-if = ["os == 'android'"] # no https-only errorpage support in android + +["test_redirect_upgrade.html"] +scheme = "https" +fail-if = ["xorigin"] +skip-if = [ + "http3", + "http2", +] + +["test_resource_upgrade.html"] +scheme = "https" +skip-if = [ + "http3", + "http2", +] + +["test_user_suggestion_box.html"] +skip-if = [ + "os == 'android'", # no https-only errorpage support in android + "http3", + "http2", +] diff --git a/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html b/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html new file mode 100644 index 0000000000..847a71f378 --- /dev/null +++ b/dom/security/test/https-only/test_break_endless_upgrade_downgrade_loop.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1691888: Break endless upgrade downgrade loops when using https-only</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> +"use strict"; +/* + * Description of the test: + * We perform three tests where our upgrade/downgrade redirect loop detector should break the + * endless loop: + * Test 1: Meta Refresh + * Test 2: JS Redirect + * Test 3: 302 redirect + */ + +SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear"); +SimpleTest.requestLongerTimeout(10); +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = + "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs"; + +function resolveAfter6Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 6000); + }); +} + +async function verifyResult(aTestName) { + let errorPageL10nId = "about-httpsonly-title-alert"; + let innerHTML = content.document.body.innerHTML; + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown for " + aTestName); +} + +async function verifyTest4Result() { + let pathname = content.document.location.pathname; + ok( + pathname === "/tests/dom/security/test/https-only/file_user_gesture.html", + "the http:// page should be loaded" + ); +} + +async function runTests() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_break_upgrade_downgrade_endless_loop", true], + ["dom.security.https_only_check_path_upgrade_downgrade_endless_loop", true], + ]}); + + // Test 1: Meta Refresh Redirect + let winTest1 = window.open(REQUEST_URL + "?test1a", "_blank"); + // Test 2: JS win.location Redirect + let winTest2 = window.open(REQUEST_URL + "?test2a", "_blank"); + // Test 3: 302 Redirect + let winTest3 = window.open(REQUEST_URL + "?test3a", "_blank"); + // Test 4: 302 Redirect with a different path + let winTest4 = window.open(REQUEST_URL + "?test4a", "_blank"); + + // provide enough time for: + // the redirects to occur, and the error page to be displayed + await resolveAfter6Seconds(); + + await SpecialPowers.spawn(winTest1, ["test1"], verifyResult); + winTest1.close(); + + await SpecialPowers.spawn(winTest2, ["test2"], verifyResult); + winTest2.close(); + + await SpecialPowers.spawn(winTest3, ["test3"], verifyResult); + winTest3.close(); + + await SpecialPowers.spawn(winTest4, ["test4"], verifyTest4Result); + winTest4.close(); + + SimpleTest.finish(); +} + +runTests(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_fragment.html b/dom/security/test/https-only/test_fragment.html new file mode 100644 index 0000000000..52a63764fb --- /dev/null +++ b/dom/security/test/https-only/test_fragment.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1694932: Https-only mode reloads the page in certain cases when there should be just a fragment navigation </title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> +"use strict"; +/* + * Description of the test: + * Perform a test where a button click leads to scroll the page. Test if https-only detects + * that the redirection address of the button is on the same page. Instead of a reload https-only + * should only scroll. + * + * Test: + * Enable https-only and load the url + * Load: http://mozilla.pettay.fi/moztests/fragment.html + * Click "Click me" + * The page should be scrolled down to 'foo' without a reload + * It shouldn't receive a message 'before unload' because the on before unload + * function in file_fragment.html should not be called + */ + +SimpleTest.waitForExplicitFinish(); + +const REQUEST_URL = "http://example.com/tests/dom/security/test/https-only/file_fragment.html"; +const EXPECT_URL = REQUEST_URL.replace("http://", "https://"); +let winTest = null; +let checkButtonClicked = false; + +async function receiveMessage(event) { + let data = event.data; + // checks if click was successful + if (!checkButtonClicked){ + // expected window location ( expected URL) + ok(data.result == EXPECT_URL, "location is correct"); + ok(data.button, "button is clicked"); + // checks if loading was successful + ok(data.info == "onload", "Onloading worked"); + // button was clicked + checkButtonClicked = true; + return; + } + // if Button was clicked once -> test finished + // check if hash exist and if hash of location is correct + ok(data.button, "button is clicked"); + ok(data.result == EXPECT_URL + "#foo", "location (hash) is correct"); + // check that page is scrolled not reloaded + ok(data.info == "scrolled-to-foo","Scrolled successfully without reloading!"); + is(data.documentURI, EXPECT_URL + "#foo", "Document URI is correct"); + // complete test and close window + window.removeEventListener("message",receiveMessage); + winTest.close(); + SimpleTest.finish(); +} + +async function runTest() { + //Test: With https-only mode activated + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ]}); + winTest = window.open(REQUEST_URL); +} +window.addEventListener("message", receiveMessage); +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_http_background_auth_request.html b/dom/security/test/https-only/test_http_background_auth_request.html new file mode 100644 index 0000000000..5a7bf665b9 --- /dev/null +++ b/dom/security/test/https-only/test_http_background_auth_request.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1665062 - HTTPS-Only: Do not cancel channel if auth is in progress</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * We send a top-level request which results in a '401 - Unauthorized' and ensure that the + * http background request does not accidentally treat that request as a potential timeout. + * We make sure that ther HTTPS-Only Mode Error Page does *NOT* show up. + */ + +const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("When Auth is in progress, HTTPS-Only page should not show up"); +SimpleTest.requestLongerTimeout(10); + +const EXPECTED_KICK_OFF_REQUEST = + "http://test1.example.com/tests/dom/security/test/https-only/file_http_background_auth_request.sjs?foo"; +const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://"); +let EXPECTED_BG_REQUEST = "http://test1.example.com/"; +let requestCounter = 0; + +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic !== "specialpowers-http-notify-request") { + return; + } + + // On Android we have other requests appear here as well. Let's make + // sure we only evaluate requests triggered by the test. + if (!data.startsWith("http://test1.example.com") && + !data.startsWith("https://test1.example.com")) { + return; + } + ++requestCounter; + if (requestCounter == 1) { + is(data, EXPECTED_KICK_OFF_REQUEST, "kick off request needs to be http"); + return; + } + if (requestCounter == 2) { + is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https"); + return; + } + if (requestCounter == 3) { + is(data, EXPECTED_BG_REQUEST, "background request needs to be http and no sensitive info"); + return; + } + ok(false, "we should never get here, but just in case"); + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.AuthBackgroundRequestExaminer = new examiner(); + +// https-only top-level background request occurs after 3 seconds, hence +// we use 4 seconds to make sure the background request did not happen. +function resolveAfter4Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 4000); + }); +} + +async function runTests() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", true], + ]}); + + let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank"); + + // Give the Auth Process and background request some time before moving on. + await resolveAfter4Seconds(); + + if (AppConstants.platform !== "android") { + is(requestCounter, 3, "three requests total (kickoff, upgraded, background)"); + } else { + // On Android, the auth request resolves and hence the background request + // is not even kicked off - nevertheless, the error page should not appear! + is(requestCounter, 2, "two requests total (kickoff, upgraded)"); + } + + await SpecialPowers.spawn(testWin, [], () => { + let innerHTML = content.document.body.innerHTML; + is(innerHTML, "", "expection page should not be displayed"); + }); + + testWin.close(); + + window.AuthBackgroundRequestExaminer.remove(); + SimpleTest.finish(); +} + +runTests(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_http_background_request.html b/dom/security/test/https-only/test_http_background_request.html new file mode 100644 index 0000000000..5dff0a5fdb --- /dev/null +++ b/dom/security/test/https-only/test_http_background_request.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1663396: Test HTTPS-Only-Mode top-level background request not leaking sensitive info</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +/* + * Description of the test: + * Send a top-level request and make sure that the the top-level https-only background request + * (a) does only use pre-path information + * (b) does not happen if the pref is set to false + */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("have to test that https-only mode background request does not happen"); +SimpleTest.requestLongerTimeout(8); + +const SJS_PATH = "tests/dom/security/test/https-only/file_http_background_request.sjs?sensitive"; + +const EXPECTED_KICK_OFF_REQUEST = "http://example.com/" + SJS_PATH; +const EXPECTED_KICK_OFF_REQUEST_LOCAL = "http://localhost:8/" + SJS_PATH; +const EXPECTED_UPGRADE_REQUEST = EXPECTED_KICK_OFF_REQUEST.replace("http://", "https://"); +let expectedBackgroundRequest = ""; +let requestCounter = 0; + +function examiner() { + SpecialPowers.addObserver(this, "specialpowers-http-notify-request"); +} +examiner.prototype = { + observe(subject, topic, data) { + if (topic !== "specialpowers-http-notify-request") { + return; + } + // On Android we have other requests appear here as well. Let's make + // sure we only evaluate requests triggered by the test. + if (!data.startsWith("http://example.com") && + !data.startsWith("https://example.com") && + !data.startsWith("http://localhost:8") && + !data.startsWith("https://localhost:8")) { + return; + } + ++requestCounter; + if (requestCounter == 1) { + ok( + data === EXPECTED_KICK_OFF_REQUEST || data === EXPECTED_KICK_OFF_REQUEST_LOCAL, + "kick off request needs to be http" + ); + return; + } + if (requestCounter == 2) { + is(data, EXPECTED_UPGRADE_REQUEST, "upgraded request needs to be https"); + return; + } + if (requestCounter == 3) { + is(data, expectedBackgroundRequest, "background request needs to be http and no sensitive info like path"); + return; + } + ok(false, "we should never get here, but just in case"); + }, + remove() { + SpecialPowers.removeObserver(this, "specialpowers-http-notify-request"); + } +} +window.BackgroundRequestExaminer = new examiner(); + +// https-only top-level background request occurs after 3 seconds, hence +// we use 4 seconds to make sure the background request did not happen. +function resolveAfter4Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 4000); + }); +} + +async function runTests() { + // (a) Test http background request to only use prePath information + expectedBackgroundRequest = "http://example.com/"; + requestCounter = 0; + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", true], + ["dom.security.https_only_mode_error_page_user_suggestions", false], + ]}); + let testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank"); + await resolveAfter4Seconds(); + is(requestCounter, 3, "three requests total (kickoff, upgraded, background)"); + testWin.close(); + + // (x) Test no http background request happens when localhost + expectedBackgroundRequest = ""; + requestCounter = 0; + testWin = window.open(EXPECTED_KICK_OFF_REQUEST_LOCAL, "_blank"); + await resolveAfter4Seconds(); + is(requestCounter, 1, "one requests total (kickoff, no upgraded, no background)"); + testWin.close(); + + // (b) Test no http background request happens if pref is set to false + expectedBackgroundRequest = ""; + requestCounter = 0; + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", false], + ["dom.security.https_only_mode_error_page_user_suggestions", false], + ]}); + testWin = window.open(EXPECTED_KICK_OFF_REQUEST, "_blank"); + await resolveAfter4Seconds(); + is(requestCounter, 2, "two requests total (kickoff, upgraded, no background)"); + testWin.close(); + + // clean up and finish tests + window.BackgroundRequestExaminer.remove(); + SimpleTest.finish(); +} + +runTests(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_insecure_reload.html b/dom/security/test/https-only/test_insecure_reload.html new file mode 100644 index 0000000000..d143c9080b --- /dev/null +++ b/dom/security/test/https-only/test_insecure_reload.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Bug 1702001: Https-only mode does not reload pages after clicking "Continue to HTTP Site", when url contains navigation </title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> +"use strict"; +/* + * Description of the test: + * + * Load a page including a fragment portion which does not support https and make + * sure that exempting the page from https-only-mode does not result in a fragment + * navigation. + */ + + + function resolveAfter6Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 6000); + }); +} + +SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear"); +SimpleTest.waitForExplicitFinish(); + +let winTest = null; +let TEST_URL = "http://example.com/tests/dom/security/test/https-only/file_insecure_reload.sjs#nav"; + +// verify that https-only page appeared +async function verifyErrorPage() { + let errorPageL10nId = "about-httpsonly-title-alert"; + let innerHTML = content.document.body.innerHTML; + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown for "); + let button = content.document.getElementById("openInsecure"); + // Click "Continue to HTTP Site" + ok(button, "button exist"); + if(button) { + button.click(); + } +} +// verify that you entered the page and are not still displaying +// the https-only error page +async function receiveMessage(event) { + // read event + let { result, historyLength } = event.data; + is(result, "you entered the http page", "The requested page should be shown"); + is(historyLength, 1, "History should contain one item"); + window.removeEventListener("message",receiveMessage); + winTest.close(); + SimpleTest.finish(); +} + + +async function runTest() { + //Test: With https-only mode activated + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ]}); + winTest = window.open(TEST_URL); + await resolveAfter6Seconds(); + await SpecialPowers.spawn(winTest,[],verifyErrorPage); +} +window.addEventListener("message", receiveMessage); +runTest(); + +</script> +</body> +</html> diff --git a/dom/security/test/https-only/test_redirect_upgrade.html b/dom/security/test/https-only/test_redirect_upgrade.html new file mode 100644 index 0000000000..59f02f96d0 --- /dev/null +++ b/dom/security/test/https-only/test_redirect_upgrade.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1613063 +Test that 302 redirect requests get upgraded to https:// with HTTPS-Only Mode enabled +--> + +<head> + <title>HTTPS-Only Mode - XHR Redirect Upgrade</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-Only Mode</h1> + <p>Upgrade Test for insecure XHR redirects.</p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1613063">Bug 1613063</a> + + <script type="application/javascript"> + + const redirectCodes = ["301", "302", "303", "307"] + let currentTest = 0 + + function startTest() { + const currentCode = redirectCodes[currentTest]; + + const myXHR = new XMLHttpRequest(); + // Make a request to a site (eg. https://file_redirect.sjs?301), which will redirect to http://file_redirect.sjs?check. + // The response will either be secure-ok, if the request has been upgraded to https:// or secure-error if it didn't. + myXHR.open("GET", `https://example.com/tests/dom/security/test/https-only/file_redirect.sjs?${currentCode}`); + myXHR.onload = (e) => { + is(myXHR.responseText, "secure-ok", `a ${currentCode} redirect when posting violation report should be blocked`) + testDone(); + } + // This should not happen + myXHR.onerror = (e) => { + ok(false, `Could not query results from server for ${currentCode}-redirect test (" + e.message + ")`); + testDone(); + } + myXHR.send(); + } + + function testDone() { + // Check if there are remaining tests + if (++currentTest < redirectCodes.length) { + startTest() + } else { + SimpleTest.finish(); + } + } + + SimpleTest.waitForExplicitFinish(); + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [["dom.security.https_only_mode", true]] }, startTest); + + </script> +</body> +</html> diff --git a/dom/security/test/https-only/test_resource_upgrade.html b/dom/security/test/https-only/test_resource_upgrade.html new file mode 100644 index 0000000000..6584bad020 --- /dev/null +++ b/dom/security/test/https-only/test_resource_upgrade.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>HTTPS-Only Mode - Resource Upgrade</title> + <!-- Including SimpleTest.js so we can use waitForExplicitFinish !--> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <h1>HTTPS-Only Mode</h1> + <p>Upgrade Test for various resources</p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1613063">Bug 1613063</a> + <iframe style="width:100%;" id="testframe"></iframe> + + <script class="testbody" type="text/javascript"> + /* Description of the test: + * We load resources (img, script, sytle, etc) over *http* and make sure + * that all the resources get upgraded to use >> https << when the + * preference "dom.security.https_only_mode" is set to true. We further + * test that subresources within nested contexts (iframes) get upgraded + * and also test the handling of server side redirects. + * + * In detail: + * We perform an XHR request to the *.sjs file which is processed async on + * the server and waits till all the requests were processed by the server. + * Once the server received all the different requests, the server responds + * to the initial XHR request with an array of results which must match + * the expected results from each test, making sure that all requests + * received by the server (*.sjs) were actually *https* requests. + */ + + const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + const splitRegex = /^(.*)-(.*)$/ + const testConfig = { + topLevelScheme: "http://", + results: [ + "iframe", "script", "img", "img-redir", "font", "xhr", "style", + "media", "object", "form", "nested-img" + ] + } + // TODO: WebSocket tests are not supported on Android Yet. Bug 1566168. + if (AppConstants.platform !== "android") { + testConfig.results.push("websocket"); + } + + + function runTest() { + // sends an xhr request to the server which is processed async, which only + // returns after the server has received all the expected requests. + var myXHR = new XMLHttpRequest(); + myXHR.open("GET", "file_upgrade_insecure_server.sjs?queryresult"); + myXHR.onload = function (e) { + var results = myXHR.responseText.split(","); + for (var index in results) { + checkResult(results[index]); + } + } + myXHR.onerror = function (e) { + ok(false, "Could not query results from server (" + e.message + ")"); + finishTest(); + } + myXHR.send(); + + // give it some time and run the testpage + SimpleTest.executeSoon(() => { + var src = testConfig.topLevelScheme + "example.com/tests/dom/security/test/https-only/file_upgrade_insecure.html"; + document.getElementById("testframe").src = src; + }); + } + + // a postMessage handler that is used by sandboxed iframes without + // 'allow-same-origin' to bubble up results back to this main page. + window.addEventListener("message", receiveMessage); + function receiveMessage(event) { + checkResult(event.data.result); + } + + function finishTest() { + window.removeEventListener("message", receiveMessage); + SimpleTest.finish(); + } + + function checkResult(response) { + // A response looks either like this "iframe-ok" or "[key]-[result]" + const [, key, result] = splitRegex.exec(response) + // try to find the expected result within the results array + var index = testConfig.results.indexOf(key); + + // If the response is not even part of the results array, something is super wrong + if (index == -1) { + ok(false, `Unexpected response from server (${response})`); + finishTest(); + } + + // take the element out the array and continue till the results array is empty + if (index != -1) { + testConfig.results.splice(index, 1); + } + + // Check if the result was okay or had an error + is(result, 'ok', `Upgrade all requests on toplevel http for '${key}' came back with: '${result}'`) + + // If we're not expecting any more resulsts, finish the test + if (!testConfig.results.length) { + finishTest(); + } + } + + SimpleTest.waitForExplicitFinish(); + + // Set preference and start test + SpecialPowers.pushPrefEnv({ set: [["dom.security.https_only_mode", true]] }, runTest); + + </script> +</body> + +</html> diff --git a/dom/security/test/https-only/test_user_suggestion_box.html b/dom/security/test/https-only/test_user_suggestion_box.html new file mode 100644 index 0000000000..1feabcd003 --- /dev/null +++ b/dom/security/test/https-only/test_user_suggestion_box.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1665057 - Add www button on https-only error page</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +"use strict"; + +/* + * Description of the test: + * We send a top-level request to a http-page in https-only mode + * The page has a bad certificate and can't be updated so the error page appears + * If there is a secure connection possible to www the suggestion-box on the error page + * should appear and have a link to that secure www-page + * if the original-pagerequest already has a www or there is no secure www connection + * the suggestion box should not appear + */ + +SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear and for the additional 'www' request to provide a suggestion."); +SimpleTest.requestLongerTimeout(10); + +// urls of server locations with bad cert -> https-only should display error page +const KICK_OFF_REQUEST_WITH_SUGGESTION = "http://suggestion-example.com"; +const KICK_OFF_REQUEST_WITHOUT_SUGGESTION = "http://no-suggestion-example.com"; + +// l10n ids to compare html to +const ERROR_PAGE_L10N_ID = "about-httpsonly-title-alert"; +const SUGGESTION_BOX_L10N_ID = "about-httpsonly-suggestion-box-www-text"; + +// the error page needs to be build and a background https://www request needs to happen +// we use 4 seconds to make sure these requests did happen. +function resolveAfter4Seconds() { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 4000); + }); +} + +async function runTests(aMessage) { + let errorPageL10nId = "about-httpsonly-title-alert"; + let suggestionBoxL10nId = "about-httpsonly-suggestion-box-www-text"; + + let innerHTML = content.document.body.innerHTML; + + if (aMessage === "with_suggestion") { + // test if the page with suggestion shows the error page and the suggestion box + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown."); + ok(innerHTML.includes(suggestionBoxL10nId), "the suggestion box should be shown."); + } else if (aMessage === "without_suggestion") { + // test if the page without suggestion shows the error page but not the suggestion box + ok(innerHTML.includes(errorPageL10nId), "the error page should be shown."); + ok(!innerHTML.includes(suggestionBoxL10nId), "the suggestion box should not be shown."); + } else { + ok(false, "we should never get here"); + } +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ set: [ + ["dom.security.https_only_mode", true], + ["dom.security.https_only_mode_send_http_background_request", false], + ["dom.security.https_only_mode_error_page_user_suggestions", true], + ]}); + let testWinSuggestion = window.open(KICK_OFF_REQUEST_WITH_SUGGESTION, "_blank"); + let testWinWithoutSuggestion = window.open(KICK_OFF_REQUEST_WITHOUT_SUGGESTION, "_blank"); + + await resolveAfter4Seconds(); + + await SpecialPowers.spawn(testWinSuggestion, ["with_suggestion"], runTests); + await SpecialPowers.spawn(testWinWithoutSuggestion, ["without_suggestion"], runTests); + + testWinSuggestion.close(); + testWinWithoutSuggestion.close(); +}); +</script> +</body> +</html> |