Bug 1747230 - Fix IsUpgradeDowngradeEndlessLoop blocking legitimate redirects when redirecting to different query parameters r=necko-reviewers,kershaw,simonf,maltejur

This changes where the IsUpgradeDowngradeEndlessLoop check triggers.
Before this patch, it triggered during the redirect caused by the https
upgrade. With this patch, it triggers during the downgrade for http
redirects. META and JS redirect are still detected during upgrade.
This should be fixed as a follow up (See Bug 1896691).
Downgrade in this context means same url, except with the scheme http
instead of https.

Different query parameters normally lead to different responses by web servers.
Don't consider the '#ref' part of the uri, because it doesn't get send to
the server and therefore can't change the server response.

We can't use the redirect chain anymore, because the query parameters
are trimmed since Bug 1715785.

This also removes the config option dom.security.https_only_check_path_upgrade_downgrade_endless_loop,
because it adds unnecessary complexity. Removing it for this patch is
easier.

https-only, https-first and httpssvc_https_upgrade tests had to be
modified, because they depended on the incorrect handling of query
strings in loop detection.

Differential Revision: https://phabricator.services.mozilla.com/D193672
This commit is contained in:
Manuel Bucher 2024-06-10 17:09:57 +00:00
Родитель 51e5807d51
Коммит ca28980109
17 изменённых файлов: 352 добавлений и 261 удалений

Просмотреть файл

@ -8533,8 +8533,9 @@ bool nsDocShell::IsSameDocumentNavigation(nsDocShellLoadState* aLoadState,
if (nsCOMPtr<nsIChannel> docChannel = GetCurrentDocChannel()) {
nsCOMPtr<nsILoadInfo> docLoadInfo = docChannel->LoadInfo();
if (!docLoadInfo->GetLoadErrorPage() &&
nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(
currentExposableURI, aLoadState->URI(), docLoadInfo)) {
nsHTTPSOnlyUtils::ShouldUpgradeConnection(docLoadInfo) &&
nsHTTPSOnlyUtils::IsHttpDowngrade(currentExposableURI,
aLoadState->URI())) {
uint32_t status = docLoadInfo->GetHttpsOnlyStatus();
if (status & (nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED |
nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) {

Просмотреть файл

@ -266,7 +266,7 @@ bool nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(nsIURI* aURI,
/* static */
bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
nsIURI* aURI, nsILoadInfo* aLoadInfo,
nsIURI* aOldURI, nsIURI* aNewURI, nsILoadInfo* aLoadInfo,
const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions) {
// 1. Check if the HTTPS-Only/HTTPS-First is even enabled, before doing
// anything else
@ -307,84 +307,47 @@ bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
return false;
}
// 5. If the URI to be loaded is not http, then it's defnitely no endless
// loop caused by https-only.
if (!aURI->SchemeIs("http")) {
return false;
}
nsAutoCString uriHost;
aURI->GetAsciiHost(uriHost);
auto uriAndPrincipalComparator = [&](nsIPrincipal* aPrincipal) {
nsAutoCString principalHost;
aPrincipal->GetAsciiHost(principalHost);
bool checkPath = mozilla::StaticPrefs::
dom_security_https_only_check_path_upgrade_downgrade_endless_loop();
if (!checkPath) {
return uriHost.Equals(principalHost);
}
nsAutoCString uriPath;
nsresult rv = aURI->GetFilePath(uriPath);
if (NS_FAILED(rv)) {
return false;
}
nsAutoCString principalPath;
aPrincipal->GetFilePath(principalPath);
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
// 5. Check HTTP 3xx 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
// asciiHost of the URI to be loaded and the asciiHost of the Principal are
// identical, then we are dealing with an upgrade downgrade scenario and we
// have to break the cycle.
if (!aLoadInfo->RedirectChain().IsEmpty()) {
nsCOMPtr<nsIPrincipal> redirectPrincipal;
for (nsIRedirectHistoryEntry* entry : aLoadInfo->RedirectChain()) {
entry->GetPrincipal(getter_AddRefs(redirectPrincipal));
if (redirectPrincipal && redirectPrincipal->SchemeIs("https") &&
uriAndPrincipalComparator(redirectPrincipal)) {
isLoop = true;
}
}
} else {
// 6.1 We should only check if this load is triggered by a user gesture
// when the redirect chain is empty, since this information is only useful
// in our case here. When the redirect chain is not empty, this load is
// defnitely triggered by redirection, not a user gesture.
if (IsHttpDowngrade(aOldURI, aNewURI)) {
return true;
}
// TODO(Bug 1896691): Don't depend on triggeringPrincipal for JS/Meta
// redirects. Call this function at the correct places instead
// 6. Bug 1725026: Disable JS/Meta loop detection when the load was triggered
// by a user gesture. This information is only when the redirect chain is
// empty. When the redirect chain is not empty, this load is definitely
// triggered by redirection, not a user gesture.
// TODO(1896685): Verify whether check is still necessary.
if (aLoadInfo->RedirectChain().IsEmpty()) {
if (aLoadInfo->GetHasValidUserGestureActivation()) {
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);
// 7. Meta redirects and JS based redirects (win.location). We detect them
// during the https upgrade internal redirect.
nsCOMPtr<nsIPrincipal> triggeringPrincipal = aLoadInfo->TriggeringPrincipal();
if (!triggeringPrincipal->SchemeIs("https")) {
return false;
}
if (isLoop && enforceForHTTPSFirstMode &&
mozilla::StaticPrefs::
dom_security_https_first_add_exception_on_failiure()) {
AddHTTPSFirstExceptionForSession(aURI, aLoadInfo);
// We detect Meta and JS based redirects during the upgrade. Check whether
// we are currently in an upgrade situation here.
if (!IsHttpDowngrade(aNewURI, aOldURI)) {
return false;
}
// If we upgrade to the same URI that the load is origining from we are
// creating a redirect loop.
bool isLoop = false;
nsresult rv = triggeringPrincipal->EqualsURI(aNewURI, &isLoop);
NS_ENSURE_SUCCESS(rv, false);
return isLoop;
}
@ -436,7 +399,8 @@ bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI,
}
// https-first needs to account for breaking upgrade-downgrade endless
// loops at this point because this function is called before we
// loops at this point for meta and js redirects because this function
// is called before we
// check the redirect limit in HttpBaseChannel. If we encounter
// a same-origin server side downgrade from e.g https://example.com
// to http://example.com then we simply not annotating the loadinfo
@ -444,12 +408,18 @@ bool nsHTTPSOnlyUtils::ShouldUpgradeHttpsFirstRequest(nsIURI* aURI,
// the handling for https-only mode is different from https-first mode,
// because https-only mode results in an exception page in case
// we encounter and endless upgrade downgrade loop.
/*
bool isUpgradeDowngradeEndlessLoop = IsUpgradeDowngradeEndlessLoop(
aURI, aLoadInfo,
aURI, aURI, aLoadInfo,
{UpgradeDowngradeEndlessLoopOptions::EnforceForHTTPSFirstMode});
if (isUpgradeDowngradeEndlessLoop) {
if (mozilla::StaticPrefs::
dom_security_https_first_add_exception_on_failiure()) {
nsHTTPSOnlyUtils::AddHTTPSFirstExceptionForSession(aURI, aLoadInfo);
}
return false;
}
*/
// We can upgrade the request - let's log to the console and set the status
// so we know that we upgraded the request.
@ -900,38 +870,51 @@ bool nsHTTPSOnlyUtils::LoopbackOrLocalException(nsIURI* aURI) {
}
/* static */
bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
nsIURI* aOtherURI,
nsILoadInfo* aLoadInfo) {
// 1. Check if one of parameters is null then webpage can't be loaded yet
bool nsHTTPSOnlyUtils::ShouldUpgradeConnection(nsILoadInfo* aLoadInfo) {
// Check if one of parameters is null then webpage can't be loaded yet
// and no further inspections are needed
if (!aHTTPSSchemeURI || !aOtherURI || !aLoadInfo) {
if (!aLoadInfo) {
return false;
}
// 2. If the URI to be loaded is not http, then same origin will be detected
// already
if (!mozilla::net::SchemeIsHTTP(aOtherURI)) {
return false;
}
// 3. Check if the HTTPS-Only Mode is even enabled, before we do anything else
// Check if the HTTPS-Only Mode is even enabled, before we do anything else
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsOnlyModeEnabled(isPrivateWin) &&
!IsHttpsFirstModeEnabled(isPrivateWin)) {
return false;
}
// 4. If the load is exempt, then it's defintely not related to https-only
// If the load is exempt, then don't upgrade
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
return false;
}
return true;
}
// 5. Create a new target URI with 'https' instead of 'http' and compare it
// to the current URI
/* static */
bool nsHTTPSOnlyUtils::IsHttpDowngrade(nsIURI* aFromURI, nsIURI* aToURI) {
MOZ_ASSERT(aFromURI);
MOZ_ASSERT(aToURI);
if (!aFromURI || !aToURI) {
return false;
}
// 2. If the target URI is not http, then it's not a http downgrade
if (!mozilla::net::SchemeIsHTTP(aToURI)) {
return false;
}
// 3. If the origin URI isn't https, then it's not a http downgrade either.
if (!mozilla::net::SchemeIsHTTPS(aFromURI)) {
return false;
}
// 4. Create a new target URI with 'https' instead of 'http' and compare it
// to the origin URI
int32_t port = 0;
nsresult rv = aOtherURI->GetPort(&port);
nsresult rv = aToURI->GetPort(&port);
NS_ENSURE_SUCCESS(rv, false);
// a port of -1 indicates the default port, hence we upgrade from port 80 to
// port 443
@ -940,15 +923,14 @@ bool nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
port = NS_GetDefaultPort("https");
}
nsCOMPtr<nsIURI> newHTTPSchemeURI;
rv = NS_MutateURI(aOtherURI)
rv = NS_MutateURI(aToURI)
.SetScheme("https"_ns)
.SetPort(port)
.Finalize(newHTTPSchemeURI);
NS_ENSURE_SUCCESS(rv, false);
bool uriEquals = false;
if (NS_FAILED(
aHTTPSSchemeURI->EqualsExceptRef(newHTTPSchemeURI, &uriEquals))) {
if (NS_FAILED(aFromURI->EqualsExceptRef(newHTTPSchemeURI, &uriEquals))) {
return false;
}

Просмотреть файл

@ -77,7 +77,7 @@ class nsHTTPSOnlyUtils {
EnforceForHTTPSRR,
};
static bool IsUpgradeDowngradeEndlessLoop(
nsIURI* aURI, nsILoadInfo* aLoadInfo,
nsIURI* aOldURI, nsIURI* aNewURI, nsILoadInfo* aLoadInfo,
const mozilla::EnumSet<UpgradeDowngradeEndlessLoopOptions>& aOptions =
{});
@ -154,16 +154,19 @@ class nsHTTPSOnlyUtils {
static bool IsSafeToAcceptCORSOrMixedContent(nsILoadInfo* aLoadInfo);
/**
* Checks if two URIs are same origin modulo the difference that
* aHTTPSchemeURI uses an http scheme.
* @param aHTTPSSchemeURI nsIURI using scheme of https
* @param aOtherURI nsIURI using scheme of http
* Checks if https only or https first mode is enabled for this load
* @param aLoadInfo nsILoadInfo of the request
*/
static bool ShouldUpgradeConnection(nsILoadInfo* aLoadInfo);
/**
* Checks if two URIs are same origin modulo the difference that
* aToURI scheme is downgraded to http from https aFromURI.
* @param aFromURI nsIURI using scheme of https
* @param aToURI nsIURI using scheme of http
* @return true, if URIs are equal except scheme and ref
*/
static bool IsEqualURIExceptSchemeAndRef(nsIURI* aHTTPSSchemeURI,
nsIURI* aOtherURI,
nsILoadInfo* aLoadInfo);
static bool IsHttpDowngrade(nsIURI* aFromURI, nsIURI* aToURI);
/**
* Will add a special temporary HTTPS-Only exception that only applies to

Просмотреть файл

@ -1,61 +1,96 @@
"use strict";
const REDIRECT_URI =
"http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?verify";
const DOWNGRADE_URI =
"http://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs";
const RESPONSE_ERROR = "unexpected-query";
// An onload postmessage to window opener
const RESPONSE_HTTPS_SCHEME = `
// DOWNGRADE_REDIRECT_*: http instead of https, otherwise same path
const DOWNGRADE_REDIRECT_META = `
<html>
<head>
<meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?downgrade_redirect_meta'">
</head>
<body>
<script type="application/javascript">
window.opener.postMessage({result: 'scheme-https'}, '*');
</script>
META REDIRECT
</body>
</html>`;
const DOWNGRADE_REDIRECT_JS = `
<html>
<body>
JS REDIRECT
<script>
let url= "http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?downgrade_redirect_js";
window.location = url;
</script>
</body>
</html>`;
// REDIRECT_*: different path and http instead of https
const REDIRECT_META = `
<html>
<head>
<meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs?redirect_meta'">
</head>
<body>
META REDIRECT
</body>
</html>`;
const REDIRECT_JS = `
<html>
<body>
JS REDIRECT
<script>
let url= "http://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs?redirect_js";
window.location = url;
</script>
</body>
</html>`;
// An onload postmessage to window opener
const RESPONSE_HTTP_SCHEME = `
<html>
<body>
<script type="application/javascript">
window.opener.postMessage({result: 'scheme-http'}, '*');
window.opener.postMessage({result: 'scheme-http-'+window.location}, '*');
</script>
</body>
</html>`;
function handleRequest(request, response) {
response.setHeader("Cache-Control", "no-cache", false);
const query = request.queryString;
if (query == "downgrade") {
// send same-origin downgrade from https: to http: with a different path.
// we don't consider it's an endless upgrade downgrade loop in this case.
response.setStatusLine(request.httpVersion, 302, "Found");
response.setHeader("Location", DOWNGRADE_URI, false);
if (request.scheme == "https") {
// allow http status code as parameter
const query = request.queryString.split("=");
if (query[0] == "downgrade_redirect_http") {
let location = `http://${request.host}${request.path}?${request.queryString}`;
response.setStatusLine(request.httpVersion, query[1], "Found");
response.setHeader("Location", location, false);
} else if (query[0] == "redirect_http") {
response.setStatusLine(request.httpVersion, query[1], "Found");
let location =
"http://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs?" +
request.queryString;
response.setHeader("Location", location, false);
} else if (query[0] == "downgrade_redirect_js") {
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(DOWNGRADE_REDIRECT_JS);
} else if (query[0] == "redirect_js") {
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(REDIRECT_JS);
} else if (query[0] == "downgrade_redirect_meta") {
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(DOWNGRADE_REDIRECT_META);
} else if (query[0] == "redirect_meta") {
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(REDIRECT_META);
} else {
// We should never get here, but just in case ...
response.setStatusLine(request.httpVersion, 500, "OK");
response.write("unexepcted query");
}
return;
}
// handle the redirect case
if ((query >= 301 && query <= 303) || query == 307) {
// send same-origin downgrade from https: to http: again simluating
// and endless upgrade downgrade loop.
response.setStatusLine(request.httpVersion, query, "Found");
response.setHeader("Location", REDIRECT_URI, false);
return;
}
// Check if scheme is http:// or https://
if (query == "verify") {
let response_content =
request.scheme === "https" ? RESPONSE_HTTPS_SCHEME : RESPONSE_HTTP_SCHEME;
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(response_content);
return;
}
// We should never get here, but just in case ...
response.setStatusLine(request.httpVersion, 500, "OK");
response.write("unexepcted query");
// return http response
response.setStatusLine(request.httpVersion, 200, "OK");
response.write(RESPONSE_HTTP_SCHEME);
}

Просмотреть файл

@ -5,7 +5,7 @@ const RESPONSE_HTTPS_SCHEME = `
<html>
<body>
<script type="application/javascript">
window.opener.postMessage({result: 'scheme-https'}, '*');
window.opener.postMessage({result: 'scheme-https-'+window.location}, '*');
</script>
</body>
</html>`;
@ -14,7 +14,7 @@ const RESPONSE_HTTP_SCHEME = `
<html>
<body>
<script type="application/javascript">
window.opener.postMessage({result: 'scheme-http'}, '*');
window.opener.postMessage({result: 'scheme-http-'+window.location}'}, '*');
</script>
</body>
</html>`;

Просмотреть файл

@ -9,6 +9,10 @@ const OTHERHOST_REDIRECT_URI_HTTP =
"http://example.org/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify";
const REDIRECT_URI_HTTPS =
"https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?verify";
const REDIRECT_DOWNGRADE_URI =
"https://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?downgrade";
const REDIRECT_DOWNGRADE_URI_HTTP =
"http://example.com/tests/dom/security/test/https-first/file_multiple_redirection.sjs?downgrade";
const RESPONSE_ERROR = "unexpected-query";
@ -52,6 +56,10 @@ function sendRedirection(query, response) {
if (query.includes("test4")) {
response.setHeader("Location", OTHERHOST_REDIRECT_URI_HTTP, false);
}
// send a redirection http downgrade uri
if (query.includes("test5")) {
response.setHeader("Location", REDIRECT_DOWNGRADE_URI, false);
}
}
function handleRequest(request, response) {
@ -64,6 +72,7 @@ function handleRequest(request, response) {
if (request.scheme !== "https") {
response.setStatusLine(request.httpVersion, 500, "OK");
response.write("Request should have been HTTPS.");
return;
}
// send a 302 redirection
response.setStatusLine(request.httpVersion, 302, "Found");
@ -75,11 +84,20 @@ function handleRequest(request, response) {
if (request.scheme !== "https") {
response.setStatusLine(request.httpVersion, 500, "OK");
response.write("Request should have been HTTPS.");
return;
}
response.setStatusLine(request.httpVersion, 302, "Found");
sendRedirection(query, response);
return;
}
// Send a http redirect downgrade
if (query.includes("downgrade")) {
response.setStatusLine(request.httpVersion, 302, "Found");
let redirect_uri =
request.scheme === "https" ? REDIRECT_DOWNGRADE_URI : REDIRECT_URI_HTTP;
response.setHeader("Location", redirect_uri, false);
return;
}
// Reset the HSTS policy, prevent influencing other tests
if (request.queryString === "reset") {
response.setHeader("Strict-Transport-Security", "max-age=0");

Просмотреть файл

@ -20,55 +20,64 @@ Test that same origin redirect does not cause endless loop with https-first enab
SimpleTest.waitForExplicitFinish();
const redirectCodes = ["301", "302","303","307"];
let testQueries = [
// Those are clear downgrades. Need to load http site
{ query: "downgrade_redirect_meta", result: "http" },
{ query: "downgrade_redirect_js", result: "http" },
{ query: "downgrade_redirect_http=301", result: "http" },
{ query: "downgrade_redirect_http=302", result: "http" },
{ query: "downgrade_redirect_http=303", result: "http" },
{ query: "downgrade_redirect_http=307", result: "http" },
// from here it isn't required to downgrade. Could be upgraded again
{ query: "redirect_meta", result: "https" },
{ query: "redirect_js", result: "https" },
{ query: "redirect_http=301", result: "https" },
{ query: "redirect_http=302", result: "https" },
{ query: "redirect_http=303", result: "https" },
{ query: "redirect_http=307", result: "https" },
];
let currentTest = 0;
// do each test two time. One time starting with https:// one time with http://
let currentTestStartWithHttps = false;
let testWin;
window.addEventListener("message", receiveMessage);
// receive message from loaded site verifying the scheme of
// the loaded document.
async function receiveMessage(event) {
let currentRedirectCode = redirectCodes[currentTest];
is(event.data.result,
"scheme-http",
"same-origin redirect results in 'http' for " + currentRedirectCode
let currentTestParams = testQueries[Math.floor(currentTest / 2)];
let expectedURI;
if(currentTestParams.result == "https") {
expectedURI = "https://example.com/tests/dom/security/test/https-first/file_downgrade_with_different_path.sjs?" + currentTestParams.query;
} else {
expectedURI = "http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?" + currentTestParams.query;
}
is(`scheme-${currentTestParams.result}-${expectedURI}`,
event.data.result,
`${currentTest}: redirect results in '${currentTestParams.result}' for ${expectedURI}`
);
testWin.close();
await SpecialPowers.removePermission(
"https-only-load-insecure",
"http://example.com"
);
if (++currentTest < redirectCodes.length) {
// each test gets run starting with http:// and https://. Therefore *2
if (++currentTest < 2 * testQueries.length) {
// start next case
startTest();
return;
}
// cleanup
window.removeEventListener("message", receiveMessage);
window.addEventListener("message", receiveMessageForDifferentPathTest);
testDifferentPath();
}
async function receiveMessageForDifferentPathTest(event) {
is(event.data.result,
"scheme-https",
"scheme should be https when the path is different"
);
testWin.close();
window.removeEventListener("message", receiveMessageForDifferentPathTest);
SimpleTest.finish();
}
async function startTest() {
const currentCode = redirectCodes[currentTest];
const currentTestParams = testQueries[Math.floor(currentTest / 2)];
const scheme = currentTest % 2 == 0 ? "https" : "http";
// Load an http:// window which gets upgraded to https://
let uri =
`http://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?${currentCode}`;
testWin = window.open(uri);
}
async function testDifferentPath() {
// Load an https:// window which gets downgraded to http://
let uri =
`https://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?downgrade`;
`${scheme}://example.com/tests/dom/security/test/https-first/file_break_endless_upgrade_downgrade_loop.sjs?${currentTestParams.query}`;
testWin = window.open(uri);
}
@ -77,7 +86,6 @@ Test that same origin redirect does not cause endless loop with https-first enab
["dom.security.https_first", true],
["security.mixed_content.block_active_content", false],
["security.mixed_content.block_display_content", false],
["dom.security.https_only_check_path_upgrade_downgrade_endless_loop", true],
]}, startTest);
</script>
</body>

Просмотреть файл

@ -21,10 +21,8 @@ Test multiple redirects using https-first and ensure the entire redirect chain i
const testCase = [
// test 1: https-first upgrades http://example.com/test1 -> https://example.com/test1
// that's redirect to https://example.com/.../redirect which then redirects
// to http://example.com/../verify. Since the last redirect is http, and the
// the redirection chain contains already example.com we expect https-first
// to downgrade the request.
{name: "test last redirect HTTP", result: "scheme-http", query: "test1" },
// to http://example.com/../verify.
{name: "test last redirect HTTP", result: "scheme-https", query: "test1" },
// test 2: https-first upgrades http://example.com/test2 -> https://example.com/test2
// that's redirect to https://example.com/.../redirect which then redirects
// to https://example.com/../verify. Since the last redirect is https, we
@ -43,6 +41,13 @@ Test multiple redirects using https-first and ensure the entire redirect chain i
// http://example.org/.../verify -upgrade-> httpS://example.ORG/.../verify
// Everything should be upgraded and accessed only via HTTPS!
{name: "test last redirect other HTTP origin gets upgraded", result: "scheme-https", query: "test4" },
// test 5: https-first upgrades http://example.com/test5 -> https://example.com/test5
// that's redirect to https://example.com/.../downgrade which then redirects
// https-first upgrades http://example.com/.../downgrade -> https://example.com/.../downgrade
// that's redirect to http://example.com/.../downgrade which which is detected as http downgrade
// to http://example.com/../verify. Since the last redirect is http, and we
// had a downgrade in the redirect chain. We load the http version
{name: "test downgrade HTTP", result: "scheme-http", query: "test5" },
]
let currentTest = 0;
let testWin;

Просмотреть файл

@ -4,7 +4,7 @@
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'">
<meta http-equiv="refresh" content="0; url='http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test1'">
</head>
<body>
META REDIRECT
@ -16,17 +16,17 @@ const REDIRECT_JS = `
<body>
JS REDIRECT
<script>
let url= "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test2b";
let url= "http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test2";
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";
"http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?test3";
const REDIRECT_302_DIFFERENT_PATH =
"http://example.com/tests/dom/security/test/https-only/file_user_gesture.html";
"http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?verify";
function handleRequest(request, response) {
// avoid confusing cache behaviour
@ -35,30 +35,35 @@ function handleRequest(request, response) {
// 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") {
if (request.scheme == "https") {
let query = request.queryString;
if (query === "test1a") {
if (query == "test1") {
response.write(REDIRECT_META);
return;
}
if (query === "test2a") {
if (query == "test2") {
response.write(REDIRECT_JS);
return;
}
if (query === "test3a") {
if (query == "test3") {
response.setStatusLine("1.1", 302, "Found");
response.setHeader("Location", REDIRECT_302, false);
return;
}
if (query === "test4a") {
if (query == "test4") {
response.setStatusLine("1.1", 302, "Found");
response.setHeader("Location", REDIRECT_302_DIFFERENT_PATH, false);
return;
}
if (query == "verify") {
response.write("<html><body>OK :)</body></html>");
return;
}
}
// we should never get here, just in case,

Просмотреть файл

@ -16,73 +16,85 @@
* Test 1: Meta Refresh
* Test 2: JS Redirect
* Test 3: 302 redirect
* Test 4: Redirect to different origin. No redirect loop should be detected
*/
SimpleTest.requestFlakyTimeout("We need to wait for the HTTPS-Only error page to appear");
SimpleTest.requestLongerTimeout(10);
SimpleTest.waitForExplicitFinish();
const REQUEST_URL =
const HTTP_REQUEST_URL =
"http://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs";
const HTTPS_REQUEST_URL =
"https://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs";
function resolveAfter6Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 6000);
});
}
const testQueries = [
// Test 1: Meta Refresh Redirect
{ scheme: "http", query: "test1", error: true },
{ scheme: "https", query: "test1", error: true },
// Test 2: JS win.location Redirect
{ scheme: "http", query: "test2", error: true },
{ scheme: "https", query: "test2", error: true },
// Test 3: 302 Redirect
{ scheme: "http", query: "test3", error: true },
{ scheme: "https", query: "test3", error: true },
// Test 4: 302 Redirect with a different path
{ scheme: "http", query: "test4", error: false },
{ scheme: "https", query: "test4", error: false },
];
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);
}
let currentTest = 0;
// do each test two time. One time starting with https:// one time with http://
let testWin;
window.addEventListener("message", receiveMessageWhenLoaded);
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"
function postMessageWhenLoaded() {
SimpleTest.waitForCondition(async () => {
return await SpecialPowers.spawn(testWin, [], () => {
let innerHTML = content.document.body.innerHTML;
return innerHTML == "OK :)"
|| innerHTML == "DO NOT DISPLAY THIS"
|| innerHTML.includes("about-httpsonly-title-alert");
}).catch(() => false);
},
() => window.postMessage("https-only-page-loaded", "*"),
"waiting for page load to complete"
);
}
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],
]});
async function receiveMessageWhenLoaded() {
const currentTestParams = testQueries[currentTest];
let testName = currentTestParams.scheme + ":" + currentTestParams.query
// 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();
let innerHTML = await SpecialPowers.spawn(testWin, [], () => {
return content.document.body.innerHTML;
});
if(currentTestParams.error) {
ok(innerHTML.includes("about-httpsonly-title-alert"), testName + ": the error page should be shown");
} else {
is(innerHTML, "OK :)", testName + ": different path with https loaded ");
}
testWin.close();
if (++currentTest < testQueries.length) {
runNextTest();
return;
}
// no more tests to run -> cleanup
window.removeEventListener("https-only-page-load", receiveMessageWhenLoaded);
SimpleTest.finish();
}
runTests();
async function runNextTest() {
const currentTestParams = testQueries[currentTest];
let uri = `${currentTestParams.scheme}://example.com/tests/dom/security/test/https-only/file_break_endless_upgrade_downgrade_loop.sjs?${currentTestParams.query}`;
testWin = window.open(uri, "_blank");
postMessageWhenLoaded();
}
SpecialPowers.pushPrefEnv({ set: [
["dom.security.https_only_mode", true],
["dom.security.https_only_mode_break_upgrade_downgrade_endless_loop", true],
["dom.security.https_only_mode_ever_enabled", true], // clear this pref at the end
]}, runNextTest);
</script>
</body>

Просмотреть файл

@ -3751,13 +3751,6 @@
value: true
mirror: always
# If true, when checking if it's upgrade downgrade cycles, the URI path will be
# also checked.
- name: dom.security.https_only_check_path_upgrade_downgrade_endless_loop
type: RelaxedAtomicBool
value: true
mirror: always
# If true and HTTPS-only mode is enabled, requests
# to local IP addresses are also upgraded
- name: dom.security.https_only_mode.upgrade_local

Просмотреть файл

@ -5980,7 +5980,8 @@ HttpBaseChannel::SetNavigationStartTimeStamp(TimeStamp aTimeStamp) {
return NS_ERROR_NOT_IMPLEMENTED;
}
nsresult HttpBaseChannel::CheckRedirectLimit(uint32_t aRedirectFlags) const {
nsresult HttpBaseChannel::CheckRedirectLimit(nsIURI* aNewURI,
uint32_t aRedirectFlags) const {
if (aRedirectFlags & nsIChannelEventSink::REDIRECT_INTERNAL) {
// for internal redirect due to auth retry we do not have any limit
// as we might restrict the number of times a user might retry
@ -6014,15 +6015,39 @@ nsresult HttpBaseChannel::CheckRedirectLimit(uint32_t aRedirectFlags) const {
// https and the page answers with a redirect (meta, 302, win.location, ...)
// then this method can break the cycle which causes the https-only exception
// page to appear. Note that https-first mode breaks upgrade downgrade endless
// loops within ShouldUpgradeHTTPSFirstRequest because https-first does not
// loops within ShouldUpgradeHttpsFirstRequest because https-first does not
// display an exception page but needs a soft fallback/downgrade.
if (nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
mURI, mLoadInfo,
mURI, aNewURI, mLoadInfo,
{nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions::
EnforceForHTTPSOnlyMode})) {
// Mark that we didn't upgrade to https due to loop detection in https-only
// mode to show https-only error page. We know that we are in https-only
// mode, because we passed `EnforceForHTTPSOnlyMode` to
// `IsUpgradeDowngradeEndlessLoop`. In other words we upgrade the request
// with https-only mode, but then immediately cancel the request.
uint32_t httpsOnlyStatus = mLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_UNINITIALIZED) {
httpsOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_UNINITIALIZED;
httpsOnlyStatus |=
nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED;
mLoadInfo->SetHttpsOnlyStatus(httpsOnlyStatus);
}
LOG(("upgrade downgrade redirect loop!\n"));
return NS_ERROR_REDIRECT_LOOP;
}
// in case of http-first mode we want to add an exception to disable the
// upgrade behavior if we have upgrade-downgrade loop to break the loop and
// load the http request next
if (mozilla::StaticPrefs::
dom_security_https_first_add_exception_on_failiure() &&
nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
mURI, aNewURI, mLoadInfo,
{nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions::
EnforceForHTTPSFirstMode})) {
nsHTTPSOnlyUtils::AddHTTPSFirstExceptionForSession(mURI, mLoadInfo);
}
return NS_OK;
}

Просмотреть файл

@ -651,7 +651,7 @@ class HttpBaseChannel : public nsHashPropertyBag,
static void CallTypeSniffers(void* aClosure, const uint8_t* aData,
uint32_t aCount);
nsresult CheckRedirectLimit(uint32_t aRedirectFlags) const;
nsresult CheckRedirectLimit(nsIURI* aNewURI, uint32_t aRedirectFlags) const;
bool MaybeWaitForUploadStreamNormalization(nsIStreamListener* aListener,
nsISupports* aContext);

Просмотреть файл

@ -80,7 +80,7 @@ nsresult InterceptedHttpChannel::SetupReplacementChannel(
return rv;
}
rv = CheckRedirectLimit(aRedirectFlags);
rv = CheckRedirectLimit(aURI, aRedirectFlags);
NS_ENSURE_SUCCESS(rv, rv);
// While we can't resume an synthetic response, we can still propagate

Просмотреть файл

@ -1165,7 +1165,7 @@ nsresult TRRServiceChannel::SetupReplacementChannel(nsIURI* aNewURI,
return rv;
}
rv = CheckRedirectLimit(aRedirectFlags);
rv = CheckRedirectLimit(aNewURI, aRedirectFlags);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aNewChannel);

Просмотреть файл

@ -703,23 +703,7 @@ nsresult nsHttpChannel::MaybeUseHTTPSRRForUpgrade(bool aShouldUpgrade,
nsAutoCString uriHost;
mURI->GetAsciiHost(uriHost);
if (gHttpHandler->IsHostExcludedForHTTPSRR(uriHost)) {
return true;
}
if (nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
mURI, mLoadInfo,
{nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions::
EnforceForHTTPSRR})) {
// Add the host to a excluded list because:
// 1. We don't need to do the same check again.
// 2. Other subresources in the same host will be also excluded.
gHttpHandler->ExcludeHTTPSRRHost(uriHost);
LOG(("[%p] skip HTTPS upgrade for host [%s]", this, uriHost.get()));
return true;
}
return false;
return gHttpHandler->IsHostExcludedForHTTPSRR(uriHost);
};
if (shouldSkipUpgradeWithHTTPSRR()) {
@ -5413,7 +5397,27 @@ nsresult nsHttpChannel::SetupReplacementChannel(nsIURI* newURI,
newURI, newChannel, preserveMethod, redirectFlags);
if (NS_FAILED(rv)) return rv;
rv = CheckRedirectLimit(redirectFlags);
nsAutoCString uriHost;
mURI->GetAsciiHost(uriHost);
// disable https-rr when encountering a downgrade from https to http.
// If the host would have https-rr dns-entries, it would be misconfigured
// due to giving us mixed signals:
// 1. the signal to upgrade all http requests to https,
// 2. but also downgrading to http on https via redirects.
// Add to exclude list for that reason
if (!gHttpHandler->IsHostExcludedForHTTPSRR(uriHost) &&
nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(
mURI, newURI, mLoadInfo,
{nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions::
EnforceForHTTPSRR})) {
// Add the host to a excluded list because:
// 1. We don't need to do the same check again.
// 2. Other subresources in the same host will be also excluded.
gHttpHandler->ExcludeHTTPSRRHost(uriHost);
LOG(("[%p] skip HTTPS upgrade for host [%s]", this, uriHost.get()));
}
rv = CheckRedirectLimit(newURI, redirectFlags);
NS_ENSURE_SUCCESS(rv, rv);
// pass on the early hint observer to be able to process `103 Early Hints`

Просмотреть файл

@ -1692,7 +1692,7 @@ function handleRequest(req, res) {
} else if (u.pathname === "/redirect_to_http") {
res.setHeader(
"Location",
`http://test.httpsrr.redirect.com:${u.query.port}/redirect_to_http`
`http://test.httpsrr.redirect.com:${u.query.port}/redirect_to_http?port=${u.query.port}`
);
res.writeHead(307);
res.end("");