Bug 1884921 - HTTPS-First should add a temporary exception for sites that it is not able to upgrade r=freddyb,simonf

- Introduce new pref `dom.security.https_first_add_exception_on_failiure`
- Add new function `nsHTTPSOnlyUtils::AddFirstExceptionForSession`, which will
  set a temporary HTTPS-First exception
- When detecting a redirect loop or when downgrading, and if the pref is set,
  call `AddFirstExceptionForSession`

Differential Revision: https://phabricator.services.mozilla.com/D204380
This commit is contained in:
Malte Juergens 2024-05-27 18:57:25 +00:00
parent 08b5db9eb3
commit 5c6f4170ce
8 changed files with 247 additions and 11 deletions

View file

@ -155,6 +155,8 @@ HTTPSOnlyFailedDowngradeAgain = Upgrading insecure request “%S” failed. Down
HTTPSOnlyUpgradeSpeculativeConnection = Upgrading insecure speculative TCP connection “%1$S” to use “%2$S”.
HTTPSFirstSchemeless = Upgrading URL loaded in the address bar without explicit protocol scheme to use HTTPS.
# LOCALIZATION NOTE: %S is the hostname for which a exception will be added;
HTTPSFirstAddingSessionException = Website does not appear to support HTTPS. Further attempts to load “http://%S” securely will be skipped temporarily.
# LOCALIZATION NOTE: %S is the URL of the blocked request;
IframeSandboxBlockedDownload = Download of “%S” was blocked because the triggering iframe has the sandbox flag set.

View file

@ -9,6 +9,7 @@
#include "mozilla/TimeStamp.h"
#include "mozilla/glean/GleanMetrics.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/OriginAttributes.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/net/DNS.h"
#include "nsContentUtils.h"
@ -334,6 +335,10 @@ bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
return uriHost.Equals(principalHost) && uriPath.Equals(principalPath);
};
// We do dot return early when we find a loop, as we still need to set an
// exception at the end.
bool isLoop = false;
// 6. Check actual redirects. If the Principal that kicked off the
// load/redirect is not https, then it's definitely not a redirect cause by
// https-only. If the scheme of the principal however is https and the
@ -346,7 +351,7 @@ bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
if (redirectPrincipal && redirectPrincipal->SchemeIs("https") &&
uriAndPrincipalComparator(redirectPrincipal)) {
return true;
isLoop = true;
}
}
} else {
@ -359,18 +364,28 @@ bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
}
}
// 7. Meta redirects and JS based redirects (win.location). If the security
// context that triggered the load is not https, then it's defnitely no
// endless loop caused by https-only. If the scheme is http however and the
// asciiHost of the URI to be loaded matches the asciiHost of the Principal,
// then we are dealing with an upgrade downgrade scenario and we have to break
// the cycle.
nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal();
if (!triggeringPrincipal->SchemeIs("https")) {
return false;
if (!isLoop) {
// 7. Meta redirects and JS based redirects (win.location). If the security
// context that triggered the load is not https, then it's defnitely no
// endless loop caused by https-only. If the scheme is http however and the
// asciiHost of the URI to be loaded matches the asciiHost of the Principal,
// then we are dealing with an upgrade downgrade scenario and we have to
// break the cycle.
nsCOMPtr<nsIPrincipal> triggeringPrincipal =
aLoadInfo->TriggeringPrincipal();
if (!triggeringPrincipal->SchemeIs("https")) {
return false;
}
isLoop = uriAndPrincipalComparator(triggeringPrincipal);
}
return uriAndPrincipalComparator(triggeringPrincipal);
if (isLoop && enforceForHTTPSFirstMode &&
mozilla::StaticPrefs::
dom_security_https_first_add_exception_on_failiure()) {
AddHTTPSFirstExceptionForSession(aURI, aLoadInfo);
}
return isLoop;
}
/* static */
@ -588,6 +603,11 @@ nsHTTPSOnlyUtils::PotentiallyDowngradeHttpsFirstRequest(
nsIScriptError::warningFlag, loadInfo,
uri, true);
if (mozilla::StaticPrefs::
dom_security_https_first_add_exception_on_failiure()) {
AddHTTPSFirstExceptionForSession(uri, loadInfo);
}
return newURI.forget();
}
@ -935,6 +955,41 @@ bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
return uriEquals;
}
/* static */
nsresult nsHTTPSOnlyUtils::AddHTTPSFirstExceptionForSession(
nsCOMPtr<nsIURI> aURI, nsILoadInfo* const aLoadInfo) {
// We need to reconstruct a principal instead of taking one from the loadinfo,
// as the permission needs a http scheme, while the passed URL or principals
// on the loadinfo may have a https scheme.
nsresult rv =
NS_MutateURI(aURI).SetScheme("http"_ns).Finalize(getter_AddRefs(aURI));
NS_ENSURE_SUCCESS(rv, rv);
mozilla::OriginAttributes oa = aLoadInfo->GetOriginAttributes();
oa.SetFirstPartyDomain(true, aURI);
nsCOMPtr<nsIPermissionManager> permMgr =
mozilla::components::PermissionManager::Service();
NS_ENSURE_TRUE(permMgr, nsresult::NS_ERROR_SERVICE_NOT_AVAILABLE);
nsCOMPtr<nsIPrincipal> principal =
mozilla::BasePrincipal::CreateContentPrincipal(aURI, oa);
nsCString host;
aURI->GetHost(host);
LogLocalizedString("HTTPSFirstAddingSessionException",
{NS_ConvertUTF8toUTF16(host)}, nsIScriptError::warningFlag,
aLoadInfo, aURI, true);
rv = permMgr->AddFromPrincipal(
principal, "https-only-load-insecure"_ns,
nsIHttpsOnlyModePermission::HTTPSFIRST_LOAD_INSECURE_ALLOW_SESSION,
nsIPermissionManager::EXPIRE_SESSION, 0);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
/* static */
uint32_t nsHTTPSOnlyUtils::GetStatusForSubresourceLoad(
uint32_t aHttpsOnlyStatus) {

View file

@ -165,6 +165,17 @@ class nsHTTPSOnlyUtils {
nsIURI* aOtherURI,
nsILoadInfo* aLoadInfo);
/**
* Will add a special temporary HTTPS-Only exception that only applies to
* HTTPS-First, and is not exposed in the UI.
* @param aURI The URL for whose HTTP principal the exception should be
* added
* @param aLoadInfo The loadinfo of the request triggering this exception to
* be added (needs to match aURI)
*/
static nsresult AddHTTPSFirstExceptionForSession(
nsCOMPtr<nsIURI> aURI, nsILoadInfo* const aLoadInfo);
/**
* Determines which HTTPS-Only status flags should get propagated to
* sub-resources or sub-documents. As sub-resources and sub-documents are

View file

@ -0,0 +1,27 @@
"use strict";
/* eslint-disable @microsoft/sdl/no-insecure-url */
const URL_B =
"http://example.com/tests/dom/security/test/https-first/file_bug_1725646_b.sjs";
const RESPONSE = `
<!DOCTYPE html>
<html>
<body>
<h1>Welcome to our insecure site!</h1>
<script type="application/javascript">
window.opener.postMessage({location: location.href}, '*');
</script>
</body>
</html>`;
function handleRequest(request, response) {
response.setHeader("Cache-Control", "no-cache", false);
if (request.scheme === "http") {
response.write(RESPONSE);
} else {
response.setStatusLine(request.httpVersion, 302, "Found");
response.setHeader("Location", URL_B, false);
}
}

View file

@ -0,0 +1,34 @@
"use strict";
/* eslint-disable @microsoft/sdl/no-insecure-url */
const URL_A =
"http://example.com/tests/dom/security/test/https-first/file_bug_1725646_a.sjs";
const URL_B =
"http://example.com/tests/dom/security/test/https-first/file_bug_1725646_b.sjs";
const RESPONSE = `
<!DOCTYPE html>
<html>
<body>
<h1>We don't support HTTPS :(</h1>
<p>You will be redirected</p>
<script type="application/javascript">
window.opener.postMessage({ location: location.href }, "*");
setTimeout(() => {
window.location = "${URL_A}";
});
</script>
</body>
</html>
`;
function handleRequest(request, response) {
response.setHeader("Cache-Control", "no-cache", false);
if (request.scheme === "http") {
response.write(RESPONSE);
} else {
response.setStatusLine(request.httpVersion, 302, "Found");
response.setHeader("Location", URL_B, false);
}
}

View file

@ -7,6 +7,9 @@ skip-if = [
["test_bad_cert.html"]
support-files = ["file_bad_cert.sjs"]
["test_bug_1725646.html"]
support-files = ["file_bug_1725646_a.sjs", "file_bug_1725646_b.sjs"]
["test_break_endless_upgrade_downgrade_loop.html"]
support-files = [
"file_break_endless_upgrade_downgrade_loop.sjs",

View file

@ -0,0 +1,97 @@
<!DOCTYPE html>
<!--
Description:
1. We visit http://example.com/A
2. HTTPS-First upgrades to https://example.com/A
3. https://example.com/A redirects us to http://example.com/B, because we
visit it via https
4. HTTPS-First fails to upgrade to https://example.com/B as it gets redirected
back to http, which means we set an HTTPS-Only/First exception for
"http://example.com"
5. http://example.com/B sends HTML informing the user that HTTPS is not
supported, and redirecting the user back to http://example.com/A via
window.location = "...".
6. The load to http://example.com/A will not be upgraded again
7. Subsequent visits of http://example.com/A will also not be upgraded
-->
<html>
<head>
<meta charset="utf-8" />
<title>HTTPS-First-Mode - Simulate site similar to bom.gov.au</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
</head>
<body>
<script class="testbody" type="text/javascript">
"use strict";
/* eslint-disable @microsoft/sdl/no-insecure-url */
const URL_A =
"http://example.com/tests/dom/security/test/https-first/file_bug_1725646_a.sjs";
const URL_B =
"http://example.com/tests/dom/security/test/https-first/file_bug_1725646_b.sjs";
SimpleTest.waitForExplicitFinish();
let testWin;
let messageNumber = 0;
async function receiveMessage(event) {
switch (messageNumber) {
case 0:
is(
event.data.location,
URL_B,
"We should land on page B after being HTTP redirected"
);
break;
case 1:
is(
event.data.location,
URL_A,
"We should land on page B after being redirected back through JS and not upgraded again"
);
ok(
await SpecialPowers.testPermission(
"https-only-load-insecure",
SpecialPowers.Ci.nsIHttpsOnlyModePermission
.HTTPSFIRST_LOAD_INSECURE_ALLOW_SESSION,
URL_A
),
"A temporary HTTPS-First exception should have been added for the site"
);
testWin.close();
testWin = window.open(URL_A);
break;
case 2:
is(event.data.location, URL_A, "We should directly land on page A");
testWin.close();
window.removeEventListener("message", this);
await SpecialPowers.removePermission(
"https-only-load-insecure",
URL_A
);
SimpleTest.finish();
break;
default:
throw Error("Received too many messages");
}
messageNumber++;
}
window.addEventListener("message", receiveMessage);
SpecialPowers.pushPrefEnv({
set: [["dom.security.https_first", true]],
}).then(() => {
testWin = window.open(URL_A);
});
</script>
</body>
</html>

View file

@ -3830,6 +3830,13 @@
value: @IS_EARLY_BETA_OR_EARLIER@
mirror: always
# If true, will add a special temporary HTTPS-First exception for a site when a
# HTTPS-First upgrade fails.
- name: dom.security.https_first_add_exception_on_failiure
type: RelaxedAtomicBool
value: true
mirror: always
- name: dom.security.unexpected_system_load_telemetry_enabled
type: bool
value: true