Bug 1804684 - Fragment navigation may change document URI scheme from https to http. r=ckerschb,nika,smaug

Differential Revision: https://phabricator.services.mozilla.com/D165282
This commit is contained in:
Tom Schuster 2023-04-12 12:35:18 +00:00
parent 06ac399c37
commit c86335b0e3
10 changed files with 129 additions and 31 deletions

View file

@ -8696,12 +8696,22 @@ bool nsDocShell::IsSameDocumentNavigation(nsDocShellLoadState* aLoadState,
// fact that the new URI is currently http), then set mSameExceptHashes to
// true and only perform a fragment navigation.
if (!aState.mSameExceptHashes) {
nsCOMPtr<nsIChannel> docChannel = GetCurrentDocChannel();
if (docChannel) {
if (nsCOMPtr<nsIChannel> docChannel = GetCurrentDocChannel()) {
nsCOMPtr<nsILoadInfo> docLoadInfo = docChannel->LoadInfo();
if (!docLoadInfo->GetLoadErrorPage()) {
if (nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(
currentExposableURI, aLoadState->URI(), docLoadInfo)) {
if (!docLoadInfo->GetLoadErrorPage() &&
nsHTTPSOnlyUtils::IsEqualURIExceptSchemeAndRef(
currentExposableURI, aLoadState->URI(), docLoadInfo)) {
uint32_t status = docLoadInfo->GetHttpsOnlyStatus();
if (status & (nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED |
nsILoadInfo::HTTPS_ONLY_UPGRADED_HTTPS_FIRST)) {
// At this point the requested URI is for sure a fragment
// navigation via HTTP and HTTPS-Only mode or HTTPS-First is
// enabled. Also it is not interfering the upgrade order of
// https://searchfox.org/mozilla-central/source/netwerk/base/nsNetUtil.cpp#2948-2953.
// Since we are on an HTTPS site the fragment
// navigation should also be an HTTPS.
// For that reason we should upgrade the URI to HTTPS.
aState.mSecureUpgradeURI = true;
aState.mSameExceptHashes = true;
}
}
@ -8788,6 +8798,22 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
nsCOMPtr<nsIURI> currentURI = mCurrentURI;
// We need to upgrade the new URI from http: to https:
nsCOMPtr<nsIURI> newURI = aLoadState->URI();
if (aState.mSecureUpgradeURI) {
MOZ_TRY(NS_GetSecureUpgradedURI(aLoadState->URI(), getter_AddRefs(newURI)));
MOZ_LOG(gSHLog, LogLevel::Debug,
("Upgraded URI to %s", newURI->GetSpecOrDefault().get()));
}
#ifdef DEBUG
if (aState.mSameExceptHashes) {
bool sameExceptHashes = false;
currentURI->EqualsExceptRef(newURI, &sameExceptHashes);
MOZ_ASSERT(sameExceptHashes);
}
#endif
// Save the position of the scrollers.
nsPoint scrollPos = GetCurScrollPos();
@ -8824,7 +8850,7 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
}
// Set the doc's URI according to the new history entry's URI.
doc->SetDocumentURI(aLoadState->URI());
doc->SetDocumentURI(newURI);
/* This is a anchor traversal within the same page.
* call OnNewURI() so that, this traversal will be
@ -8854,8 +8880,7 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
newCsp = doc->GetCsp();
}
uint32_t locationChangeFlags =
GetSameDocumentNavigationFlags(aLoadState->URI());
uint32_t locationChangeFlags = GetSameDocumentNavigationFlags(newURI);
// Pass true for aCloneSHChildren, since we're not
// changing documents here, so all of our subframes are
@ -8869,10 +8894,9 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
// Note: we'll actually fire onLocationChange later, in order to preserve
// ordering of HistoryCommit() in the parent vs onLocationChange (bug
// 1668126)
bool locationChangeNeeded =
OnNewURI(aLoadState->URI(), nullptr, newURITriggeringPrincipal,
newURIPrincipalToInherit, newURIPartitionedPrincipalToInherit,
newCsp, false, true, true);
bool locationChangeNeeded = OnNewURI(
newURI, nullptr, newURITriggeringPrincipal, newURIPrincipalToInherit,
newURIPartitionedPrincipalToInherit, newCsp, false, true, true);
nsCOMPtr<nsIInputStream> postData;
uint32_t cacheKey = 0;
@ -9039,15 +9063,13 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
MOZ_LOG(gSHLog, LogLevel::Debug,
("Creating an active entry on nsDocShell %p to %s", this,
aLoadState->URI()->GetSpecOrDefault().get()));
newURI->GetSpecOrDefault().get()));
if (mActiveEntry) {
mActiveEntry =
MakeUnique<SessionHistoryInfo>(*mActiveEntry, aLoadState->URI());
mActiveEntry = MakeUnique<SessionHistoryInfo>(*mActiveEntry, newURI);
} else {
mActiveEntry = MakeUnique<SessionHistoryInfo>(
aLoadState->URI(), newURITriggeringPrincipal,
newURIPrincipalToInherit, newURIPartitionedPrincipalToInherit,
newCsp, mContentTypeHint);
newURI, newURITriggeringPrincipal, newURIPrincipalToInherit,
newURIPartitionedPrincipalToInherit, newCsp, mContentTypeHint);
}
// Save the postData obtained from the previous page in to the session
@ -9086,7 +9108,7 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
}
if (locationChangeNeeded) {
FireOnLocationChange(this, nullptr, aLoadState->URI(), locationChangeFlags);
FireOnLocationChange(this, nullptr, newURI, locationChangeFlags);
}
/* Restore the original LSHE if we were loading something
@ -9097,13 +9119,13 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
/* Set the title for the Global History entry for this anchor url.
*/
UpdateGlobalHistoryTitle(aLoadState->URI());
UpdateGlobalHistoryTitle(newURI);
SetDocCurrentStateObj(mOSHE, mActiveEntry.get());
// Inform the favicon service that the favicon for oldURI also
// applies to aLoadState->URI().
CopyFavicon(currentURI, aLoadState->URI(), UsePrivateBrowsing());
// applies to newURI.
CopyFavicon(currentURI, newURI, UsePrivateBrowsing());
RefPtr<nsGlobalWindowOuter> scriptGlobal = mScriptGlobal;
RefPtr<nsGlobalWindowInner> win =
@ -9158,7 +9180,7 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
if (doHashchange) {
// Note that currentURI hasn't changed because it's on the
// stack, so we can just use it directly as the old URI.
win->DispatchAsyncHashchange(currentURI, aLoadState->URI());
win->DispatchAsyncHashchange(currentURI, newURI);
}
}

View file

@ -1057,11 +1057,15 @@ class nsDocShell final : public nsDocLoader,
bool mCurrentURIHasRef = false;
bool mNewURIHasRef = false;
bool mSameExceptHashes = false;
bool mSecureUpgradeURI = false;
bool mHistoryNavBetweenSameDoc = false;
};
// Check to see if we're loading a prior history entry or doing a fragment
// navigation in the same document.
// NOTE: In case we are doing a fragment navigation, and HTTPS-Only/ -First
// mode is enabled and upgraded the underlying document, we update the URI of
// aLoadState from HTTP to HTTPS (if neccessary).
bool IsSameDocumentNavigation(nsDocShellLoadState* aLoadState,
SameDocumentNavigationState& aState);

View file

@ -26,8 +26,9 @@ window.onload = function (){
window.onscroll = function(){
window.opener.postMessage({
info: "scrolled-to-foo",
result: window.location.hash,
result: window.location.href,
button: true,
documentURI: document.documentURI,
}, "*");
}

View file

@ -35,8 +35,9 @@ async function receiveMessage(event) {
// Once the button was clicked we know the tast has finished
ok(data.button, "button is clicked");
ok(data.result == "#foo", "location (hash) is correct");
is(data.result, EXPECT_URL + "#foo", "location (hash) is correct");
ok(data.info == "scrolled-to-foo","Scrolled successfully without reloading!");
is(data.documentURI, EXPECT_URL + "#foo", "Document URI is correct");
window.removeEventListener("message",receiveMessage);
winTest.close();
SimpleTest.finish();

View file

@ -27,6 +27,7 @@ support-files =
[browser_hsts_host.js]
support-files =
hsts_headers.sjs
file_fragment_noscript.html
[browser_httpsonly_speculative_connect.js]
support-files = file_httpsonly_speculative_connect.html
[browser_websocket_exceptions.js]

View file

@ -33,6 +33,7 @@ add_task(async function() {
await SpecialPowers.pushPrefEnv({
set: [["dom.security.https_only_mode", true]],
});
Services.console.registerListener(onNewMessage);
const RESOURCE_LINK =
getRootDirectory(gTestPath).replace(
@ -47,6 +48,64 @@ add_task(async function() {
// 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() {

View file

@ -25,14 +25,14 @@ window.onload = function (){
}, "*");
// 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.hash,
result: window.location.href,
button: true,
documentURI: document.documentURI,
}, "*");
}

View file

@ -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>

View file

@ -27,6 +27,7 @@
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;
@ -35,7 +36,7 @@ async function receiveMessage(event) {
// checks if click was successful
if (!checkButtonClicked){
// expected window location ( expected URL)
ok(data.result == "https://example.com/tests/dom/security/test/https-only/file_fragment.html", "location is correct");
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");
@ -46,9 +47,10 @@ async function receiveMessage(event) {
// 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 == "#foo", "location (hash) is correct");
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();

View file

@ -495,8 +495,7 @@ interface nsILoadInfo : nsISupports
const unsigned long HTTPS_ONLY_DO_NOT_LOG_TO_CONSOLE = (1 << 6);
/**
* This flag indicates that the request should not be logged to the
* console.
* This flag indicates that the request was upgraded by https-first mode.
*/
const unsigned long HTTPS_ONLY_UPGRADED_HTTPS_FIRST = (1 << 7);