Bug 1691888: Break endless upgrade downgrade loops when using https-only r=necko-reviewers,valentin,JulianWels

Differential Revision: https://phabricator.services.mozilla.com/D106475
This commit is contained in:
Christoph Kerschbaumer 2021-03-11 18:02:27 +00:00
Родитель 14a5c74d1f
Коммит 39ef03a187
7 изменённых файлов: 247 добавлений и 0 удалений

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

@ -17,6 +17,7 @@
#include "nsIHttpsOnlyModePermission.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
#include "nsIRedirectHistoryEntry.h"
#include "nsIScriptError.h"
#include "prnetdb.h"
@ -205,6 +206,84 @@ bool nsHTTPSOnlyUtils::ShouldUpgradeWebSocket(nsIURI* aURI,
return true;
}
/* static */
bool nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(nsIURI* aURI,
nsILoadInfo* aLoadInfo) {
// 1. Check if the HTTPS-Only Mode is even enabled, before we do anything else
bool isPrivateWin = aLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0;
if (!IsHttpsOnlyModeEnabled(isPrivateWin)) {
return false;
}
// 2. Check if the upgrade downgrade pref even wants us to try to break the
// cycle.
if (!mozilla::StaticPrefs::
dom_security_https_only_mode_break_upgrade_downgrade_endless_loop()) {
return false;
}
// 3. If it's not a top-level load, then there is nothing to do here either.
if (aLoadInfo->GetExternalContentPolicyType() !=
ExtContentPolicy::TYPE_DOCUMENT) {
return false;
}
// 4. If the load is exempt, then it's defintely not related to https-only
uint32_t httpsOnlyStatus = aLoadInfo->GetHttpsOnlyStatus();
if (httpsOnlyStatus & nsILoadInfo::HTTPS_ONLY_EXEMPT) {
return false;
}
// 5. If the load is triggered by a user gesture, then it's definitely
// not a loop we need to break.
if (aLoadInfo->GetHasValidUserGestureActivation()) {
return false;
}
// 6. 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);
// 7. 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
// 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")) {
nsAutoCString redirectHost;
redirectPrincipal->GetAsciiHost(redirectHost);
if (uriHost.Equals(redirectHost)) {
return true;
}
}
}
}
// 8. 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;
}
nsAutoCString triggeringHost;
triggeringPrincipal->GetAsciiHost(triggeringHost);
return uriHost.Equals(triggeringHost);
}
/* static */
bool nsHTTPSOnlyUtils::CouldBeHttpsOnlyError(nsIChannel* aChannel,
nsresult aError) {

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

@ -49,6 +49,20 @@ class nsHTTPSOnlyUtils {
*/
static bool ShouldUpgradeWebSocket(nsIURI* aURI, nsILoadInfo* aLoadInfo);
/**
* Determines if we might get stuck in an upgrade-downgrade-endless loop
* where https-only upgrades the request to https and the website downgrades
* the scheme to http again causing an endless upgrade downgrade loop. E.g.
* https-only upgrades to https and the website answers with a meta-refresh
* to downgrade to same-origin http version. Similarly this method breaks
* the endless cycle for JS based redirects and 302 based redirects.
* @param aURI nsIURI of request
* @param aLoadInfo nsILoadInfo of request
* @return true if an endless loop is detected
*/
static bool IsUpgradeDowngradeEndlessLoop(nsIURI* aURI,
nsILoadInfo* aLoadInfo);
/**
* Checks if the error code is on a block-list of codes that are probably not
* related to a HTTPS-Only Mode upgrade.

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

@ -0,0 +1,58 @@
// 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";
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;
}
}
// we should never get here, just in case,
// let's return something unexpected
response.write("<html><body>DO NOT DISPLAY THIS</body></html>")
}

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

@ -16,6 +16,10 @@ fail-if = xorigin
support-files = file_http_background_request.sjs
[test_http_background_auth_request.html]
support-files = file_http_background_auth_request.sjs
[test_break_endless_upgrade_downgrade_loop.html]
skip-if = (toolkit == 'android') # no support for error pages, Bug 1697866
support-files = file_break_endless_upgrade_downgrade_loop.sjs
[test_user_suggestion_box.html]
support-files = file_user_suggestion_box.sjs
skip-if = toolkit == 'android' # no https-only errorpage support in android

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

@ -0,0 +1,75 @@
<!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 resolveAfter5Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
}, 5000);
});
}
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 runTests() {
await SpecialPowers.pushPrefEnv({ set: [
["dom.security.https_only_mode", true],
["dom.security.https_only_mode_break_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");
// provide enough time for:
// the redirects to occur, and the error page to be displayed
await resolveAfter5Seconds();
await SpecialPowers.spawn(winTest1, ["test1"], verifyResult);
winTest1.close();
await SpecialPowers.spawn(winTest2, ["test2"], verifyResult);
winTest2.close();
await SpecialPowers.spawn(winTest3, ["test3"], verifyResult);
winTest3.close();
SimpleTest.finish();
}
runTests();
</script>
</body>
</html>

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

@ -2785,6 +2785,13 @@
value: true
mirror: always
# If true, tries to break upgrade downgrade cycles where https-only tries
# to upgrad ethe connection, but the website tries to downgrade again.
- name: dom.security.https_only_mode_break_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

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

@ -31,6 +31,7 @@
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/CanonicalBrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/nsHTTPSOnlyUtils.h"
#include "mozilla/dom/Performance.h"
#include "mozilla/dom/PerformanceStorage.h"
#include "mozilla/dom/WindowGlobalParent.h"
@ -5097,6 +5098,15 @@ nsresult HttpBaseChannel::CheckRedirectLimit(uint32_t aRedirectFlags) const {
return NS_ERROR_REDIRECT_LOOP;
}
// in case https-only mode is enabled which upgrades top-level requests to
// 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.
if (nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop(mURI, mLoadInfo)) {
LOG(("upgrade downgrade redirect loop!\n"));
return NS_ERROR_REDIRECT_LOOP;
}
return NS_OK;
}