Bug 266554 - Send Referer header on refresh. r=peterv,ckerschb,dom-core,smaug

This applies for refreshes resulting from either a `<meta>` refresh or
the Refresh header. Referrer Policy is honored.

Because exposing the referrer in a new place could have privacy
implications, this behavior is gated behind a disabled pref until
anti-tracking has been considered in bug 1928294.

Some WPTs that D227450 touches are cleaned up a bit.

Differential Revision: https://phabricator.services.mozilla.com/D227450
This commit is contained in:
Zach Hoffman 2024-11-14 02:09:02 +00:00
parent 39159ab795
commit 9d26826a63
20 changed files with 478 additions and 22 deletions

View File

@ -5067,13 +5067,15 @@ nsDocShell::ForceRefreshURI(nsIURI* aURI, nsIPrincipal* aPrincipal,
loadState->SetLoadType(LOAD_REFRESH);
}
/* We mimic HTTP, which passes the original referrer.
* TODO(bug 266554): Send the referrer to the server (if allowed by referrer
* policy and tracking protection).
* See step 3 of
const bool sendReferrer = StaticPrefs::network_http_referer_sendFromRefresh();
/* The document's referrer policy is needed instead of mReferrerInfo's
* referrer policy.
*/
const nsCOMPtr<nsIReferrerInfo> referrerInfo =
new ReferrerInfo(*doc, sendReferrer);
/* We mimic HTTP, which passes the original referrer. See step 3 of
* <https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching>.
*/
const nsCOMPtr<nsIReferrerInfo> referrerInfo = new ReferrerInfo(*doc, false);
loadState->SetReferrerInfo(referrerInfo);
loadState->SetLoadFlags(

View File

@ -2,6 +2,7 @@
tags = "condprof"
prefs = [
"formhelper.autozoom.force-disable.test-only=true",
"network.http.referer.sendFromRefresh=false",
"plugins.rewrite_youtube_embeds=true",
]
support-files = [

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name='referrer' content='origin'>
<title>Test for referrer of meta refresh request</title>
<title>Test for referrer of meta refresh request when network.http.referer.sendFromRefresh is disabled.</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>

View File

@ -12525,6 +12525,13 @@
value: 500
mirror: always
# Whether to send the Referer header in response to a meta refresh, or
# in response to a Refresh header.
- name: network.http.referer.sendFromRefresh
type: bool
value: false
mirror: always
# false=real referer, true=spoof referer (use target URI as referer).
- name: network.http.referer.spoofSource
type: bool

View File

@ -0,0 +1 @@
prefs: [network.http.referer.sendFromRefresh:true]

View File

@ -0,0 +1 @@
prefs: [network.http.referer.sendFromRefresh:true]

View File

@ -0,0 +1 @@
prefs: [network.http.referer.sendFromRefresh:true]

View File

@ -0,0 +1,20 @@
[refresh-cross-origin.sub.html]
[cross-origin meta refresh with referrer policy "no-referrer-when-downgrade" refreshes with full url as referrer]
# This is wontfix behavior.
bug: 'https://bugzilla.mozilla.org/show_bug.cgi?id=1800070#c2'
expected: FAIL
[cross-origin header refresh with referrer policy "no-referrer-when-downgrade" refreshes with full url as referrer]
# This is wontfix behavior.
bug: 'https://bugzilla.mozilla.org/show_bug.cgi?id=1800070#c2'
expected: FAIL
[cross-origin meta refresh with referrer policy "unsafe-url" refreshes with full url as referrer]
# This is wontfix behavior.
bug: 'https://bugzilla.mozilla.org/show_bug.cgi?id=1800070#c2'
expected: FAIL
[cross-origin header refresh with referrer policy "unsafe-url" refreshes with full url as referrer]
# This is wontfix behavior.
bug: 'https://bugzilla.mozilla.org/show_bug.cgi?id=1800070#c2'
expected: FAIL

View File

@ -0,0 +1,42 @@
<!doctype html>
<meta charset="utf-8">
<title>Referrer from Refresh after Same-Document Navigation</title>
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=266554">
<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics.html#pragma-directives:navigate">
<link rel="author" title="Zach Hoffman" href="mailto:zach@zrhoffman.net">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
promise_test(async t => {
const refreshTo = new URL("resources/refreshed.txt", location).href;
const refreshFrom = new URL("resources/refresh-with-section.sub.html", location).href + "?" + new URLSearchParams({url: refreshTo});
const frame = document.createElement("iframe");
const { promise: frameLoaded, resolve: resolveFrameLoaded } = Promise.withResolvers();
let loadCount = 0;
frame.addEventListener("load", t.step_func(() => {
loadCount++;
try {
if (loadCount === 1) {
assert_equals(frame.contentWindow.location.href, refreshFrom + "#section", "same-document navigation occurred");
assert_equals(frame.contentWindow.referrer.textContent, location.href, "referrer header is parent frame");
assert_equals(frame.contentDocument.referrer, location.href, "document referrer is parent frame");
}
} finally {
if (loadCount === 2) {
resolveFrameLoaded();
}
}
}));
frame.src = refreshFrom;
document.body.appendChild(frame);
await frameLoaded;
assert_equals(frame.contentWindow.location.href, refreshTo, "refresh page has expected URL")
assert_equals(frame.contentDocument.referrer, frame.src, "referrer does not include fragment");
});
</script>
</body>

View File

@ -1,9 +1,19 @@
async_test(t => {
var loadCount = 0;
var expectedReferrer = location.href;
const frame = document.createElement("iframe");
frame.src = "resources/refresh.py"
var originalPath = "resources/refresh.py";
frame.src = originalPath;
frame.onload = t.step_func(() => {
// Could be better by verifying that resources/refresh.py loads too
if(frame.contentWindow.location.href === (new URL("resources/refreshed.txt?\u0080\u00FF", self.location)).href) { // Make sure bytes got mapped to code points of the same value
loadCount++;
if (loadCount === 1) {
assert_equals(frame.contentWindow.location.href, new URL(originalPath, self.location).href, "original page loads");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "referrer is parent frame");
expectedReferrer = frame.src;
} else if (loadCount === 2) {
assert_equals(frame.contentWindow.location.href,
new URL("resources/refreshed.txt?\u0080\u00FF", self.location).href, "bytes got mapped to code points of the same value");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "referrer is previous page");
t.done();
}
});
@ -11,11 +21,20 @@ async_test(t => {
}, "When navigating the Refresh header needs to be followed");
async_test(t => {
var loadCount = 0;
var expectedReferrer = location.href;
const frame = document.createElement("iframe");
frame.src = "resources/multiple.asis"
var originalPath = "resources/multiple.asis";
frame.src = originalPath
frame.onload = t.step_func(() => {
// Could be better by verifying that resources/refresh.py loads too
if(frame.contentWindow.location.href === (new URL("resources/refreshed.txt", self.location)).href) {
loadCount++;
if (loadCount === 1) {
assert_equals(frame.contentWindow.location.href, new URL(originalPath, self.location).href, "original page loads");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "referrer is parent frame");
expectedReferrer = frame.src;
} else if (loadCount === 2) {
assert_equals(frame.contentWindow.location.href, new URL("resources/refreshed.txt", self.location).href, "refresh page loads");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "referrer is previous page");
t.done();
}
});

View File

@ -0,0 +1,35 @@
<!doctype html>
<meta charset="utf-8">
<p id="referrer">{{header_or_default(referer, )}}</p>
<section id="section">My section</section>
<span id="info">Refreshing to</span>
<span id=url>{{GET[url]}}</span>
<script>
function refresh() {
if (url.textContent !== "") {
const refresh = document.createElement("meta");
refresh.httpEquiv = "refresh";
refresh.content = `0; url=${url.textContent}`;
document.documentElement.appendChild(refresh);
} else {
info.textContent = "Not refreshing.";
}
}
function sendData() {
const documentReferrer = document.referrer;
const data = {referrer: referrer.textContent, documentReferrer, url: location.href};
window.parent.postMessage(data, "*");
}
const sectionHash = "#section";
if (url.textContent !== sectionHash) {
window.addEventListener("hashchange", refresh);
location.hash = sectionHash;
} else if (location.hash !== sectionHash) {
window.addEventListener("hashchange", sendData);
refresh();
} else {
sendData();
}
</script>

View File

@ -0,0 +1,54 @@
<!doctype html>
<meta charset="utf-8">
<title>Same-Document Referrer from Refresh</title>
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=266554">
<link rel="help" href="https://github.com/whatwg/html/issues/6451">
<link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-fragid-step">
<link rel="help" href="https://html.spec.whatwg.org/multipage/semantics.html#pragma-directives:navigate">
<link rel="author" title="Zach Hoffman" href="mailto:zach@zrhoffman.net">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<script>
promise_test(async t => {
const fragment = "#section";
const refreshFrom = new URL("resources/refresh-with-section.sub.html", location).href + "?" + new URLSearchParams({url: fragment});
const frame = document.createElement("iframe");
const { promise: frameLoaded, resolve: resolveFrameLoaded } = Promise.withResolvers();
const { promise: messageHandled, resolve: resolveMessageHandled } = Promise.withResolvers();
let loadCount = 0;
frame.addEventListener("load", t.step_func(() => {
loadCount++;
try {
if (loadCount === 1) {
assert_equals(frame.contentWindow.location.href, refreshFrom, "original page loads");
assert_equals(frame.contentWindow.referrer.textContent, location.href, "referrer header is parent frame");
assert_equals(frame.contentDocument.referrer, location.href, "document referrer is parent frame");
}
} finally {
if (loadCount === 1) {
resolveFrameLoaded();
}
}
}));
addEventListener("message", t.step_func(msg => {
const {referrer, documentReferrer, url} = msg.data;
try {
assert_equals(url, refreshFrom + fragment, "refresh page has expected URL");
assert_equals(referrer, location.href, "referrer header is unchanged");
assert_equals(documentReferrer, location.href, "document referrer is unchanged");
} finally {
resolveMessageHandled();
}
}));
frame.src = refreshFrom;
document.body.appendChild(frame);
await frameLoaded;
await messageHandled;
});
</script>
</body>

View File

@ -118,11 +118,13 @@ tests_arr.forEach(function(test_obj) {
iframe.src = "support/" + filename + "?input=" + encodeURIComponent(test_obj.input);
document.body.appendChild(iframe);
var loadCount = 0;
var expectedReferrer = location.href;
iframe.onload = t.step_func(function() {
loadCount++;
var got = iframe.contentDocument.body.textContent.trim();
var content = iframe.contentDocument.body.textContent.trim();
if (test_obj.expected.length === 0) {
assert_equals(got, filename);
assert_equals(content, filename, "page has expected content");
assert_equals(iframe.contentDocument.referrer, expectedReferrer, "referrer is parent frame");
if (loadCount === 1) {
t.step_timeout(function() {
t.done();
@ -130,15 +132,16 @@ tests_arr.forEach(function(test_obj) {
} else {
assert_unreached('Got > 1 load events');
}
} else {
if (loadCount === 2) {
if(test_obj.expected[1] === "__filename__") {
assert_equals(got, filename);
} else {
assert_equals(got, test_obj.expected[1]);
}
t.done();
} else if (loadCount === 2) {
if(test_obj.expected[1] === "__filename__") {
assert_equals(content, filename, "page has expected content");
} else {
assert_equals(content, test_obj.expected[1], "page has expected content");
}
assert_equals(iframe.contentDocument.referrer, expectedReferrer, "referrer is previous page");
t.done();
} else if (loadCount === 1) {
expectedReferrer = iframe.src;
}
});
}, type + ": " + format_value(test_obj.input));

View File

@ -0,0 +1,51 @@
<!doctype html>
<meta charset="utf-8">
<title>Cross-Origin Referrer Policy applied to Refresh</title>
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=266554">
<link rel="help" href="https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer">
<link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#populating-a-session-history-entry:concept-request">
<link rel="help" href="https://github.com/privacycg/proposals/issues/13">
<link rel="author" title="Zach Hoffman" href="mailto:zach@zrhoffman.net">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/refresh-by-host.js"></script>
<body>
<script>
const ports = {
http: "{{ports[http][0]}}",
https: "{{ports[https][0]}}",
};
let scheme;
const originScheme = location.protocol.slice(0, -1);
if (originScheme === "http") {
scheme = originScheme;
} else {
scheme = "https";
}
const port = ports[scheme];
const origin = `${scheme}://{{hosts[alt][]}}:${port}`;
const path = "resources/referrer-info.sub.html";
const base = new URL(location.pathname, origin);
const url = new URL(path, base).href;
const expectationsByPolicy = {
"no-referrer": kExpectEmptyString,
// WebKit and Gecko send the origin for no-referrer-when-downgrade Referrer Policy refreshes in an
// iframe, but per the spec, the full URL should be sent in this case. Further discussion:
// <https://github.com/privacycg/proposals/issues/13>
"no-referrer-when-downgrade": kExpectFullURL,
"origin": kExpectOrigin,
"origin-when-cross-origin": kExpectOrigin,
"same-origin": kExpectEmptyString,
"strict-origin": kExpectOrigin,
"strict-origin-when-cross-origin": kExpectOrigin,
// WebKit and Gecko send the origin for unsafe-url Referrer Policy refreshes in an iframe, but per
// the spec, the full URL should be sent in this case. Further discussion:
// <https://github.com/privacycg/proposals/issues/13>
"unsafe-url": kExpectFullURL,
"": kExpectOrigin,
};
refreshWithPoliciesTest(url, expectationsByPolicy);
</script>

View File

@ -0,0 +1,28 @@
<!doctype html>
<meta charset="utf-8">
<title>Same-Origin Referrer Policy applied to Refresh</title>
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=266554">
<link rel="help" href="https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer">
<link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#populating-a-session-history-entry:concept-request">
<link rel="author" title="Zach Hoffman" href="mailto:zach@zrhoffman.net">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/refresh-by-host.js"></script>
<body>
<script>
const path = "resources/referrer-info.sub.html";
const url = new URL(path, location).href;
const expectationsByPolicy = {
"no-referrer": kExpectEmptyString,
"no-referrer-when-downgrade": kExpectFullURL,
"origin": kExpectOrigin,
"origin-when-cross-origin": kExpectFullURL,
"same-origin": kExpectFullURL,
"strict-origin": kExpectOrigin,
"strict-origin-when-cross-origin": kExpectFullURL,
"unsafe-url": kExpectFullURL,
"": kExpectFullURL,
};
refreshWithPoliciesTest(url, expectationsByPolicy);
</script>

View File

@ -0,0 +1,70 @@
<!doctype html>
<meta charset="utf-8">
<title>Same-URL Referrer Policy applied to Refresh</title>
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=266554">
<link rel="help" href="https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer">
<link rel="help" href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#populating-a-session-history-entry:concept-request">
<link rel="author" title="Zach Hoffman" href="mailto:zach@zrhoffman.net">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="./resources/refresh-by-host.js"></script>
<body>
<script>
const expectationsByPolicy = {
"no-referrer": kExpectEmptyString,
"no-referrer-when-downgrade": kExpectFullURL,
"origin": kExpectOrigin,
"origin-when-cross-origin": kExpectFullURL,
"same-origin": kExpectFullURL,
"strict-origin": kExpectOrigin,
"strict-origin-when-cross-origin": kExpectFullURL,
"unsafe-url": kExpectFullURL,
"": kExpectFullURL,
};
refreshWithPoliciesSameURLTest(expectationsByPolicy);
function refreshWithPoliciesSameURLTest(aExpectationsByPolicy) {
Object.entries(aExpectationsByPolicy).forEach(([policy, expected]) =>
Object.entries(kRefreshOptionsByDescription).forEach(([description, refreshFrom]) => {
let expectedURL = new URL(refreshFrom, location).href;
const originalURL = expectedURL + "?" + new URLSearchParams({url: expectedURL, policy});
let expectedReferrer = location.href;
promise_test(async t => {
let loadCount = 0;
const { promise: frameLoaded, resolve: resolveFrameLoaded } = Promise.withResolvers();
const frame = document.createElement("iframe");
try {
frame.addEventListener("load", t.step_func(() => {
loadCount++;
try {
if (loadCount === 1) {
assert_equals(frame.contentWindow.location.href, originalURL, "original page loads");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "referrer is parent frame");
expectedReferrer = referrerPolicyExpectationValue(expected, frame);
} else if (loadCount === 2) {
assert_equals(frame.contentWindow.location.href, expectedURL, "refresh page has expected URL");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "document referrer is same page");
}
} finally {
if (loadCount === 2) {
resolveFrameLoaded();
}
}
}));
frame.src = originalURL;
document.body.appendChild(frame);
await frameLoaded;
} finally {
frame.remove();
t.done();
}
}, `same-URL ${description} with referrer policy "${policy}" refreshes with ${expected} as referrer`);
}));
}
</script>

View File

@ -0,0 +1,8 @@
<!doctype html>
<meta charset="utf-8">
<p id="referrer">{{header_or_default(referer, )}}</p>
<script>
const documentReferrer = document.referrer;
const data = {referrer: referrer.textContent, documentReferrer, url: location.href};
window.parent.postMessage(data, "*");
</script>

View File

@ -0,0 +1,83 @@
const kOriginTypeDescriptions = {
true: "same-origin",
false: "cross-origin",
}
const kRefreshOptionsByDescription = {
"meta refresh": "resources/refresh-policy.sub.html",
"header refresh": "resources/refresh-policy.py",
};
const kExpectEmptyString = "the empty string";
const kExpectOrigin = "origin";
const kExpectFullURL = "full url";
function referrerPolicyExpectationValue(aExpected, aFrame) {
let expectedReferrer;
switch (aExpected) {
case kExpectEmptyString:
expectedReferrer = "";
break;
case kExpectOrigin:
expectedReferrer = new URL(aFrame.src).origin + "/";
break;
case kExpectFullURL:
expectedReferrer = aFrame.src;
break;
default:
throw new Error(`unexpected referrer type ${aExpected}`);
}
return expectedReferrer;
}
function refreshWithPoliciesTest(aExpectedURL, aExpectationsByPolicy) {
const isSameOrigin = location.origin === new URL(aExpectedURL).origin;
Object.entries(aExpectationsByPolicy).forEach(([policy, expected]) =>
Object.entries(kRefreshOptionsByDescription).forEach(([description, refreshFrom]) =>
promise_test(async t => {
const originalPath = refreshFrom + "?" + new URLSearchParams({url: aExpectedURL, policy});
let expectedReferrer = location.href;
let loadCount = 0;
const { promise: frameLoaded, resolve: resolveFrameLoaded } = Promise.withResolvers();
const { promise: messageHandled, resolve: resolveMessageHandled } = Promise.withResolvers();
const frame = document.createElement("iframe");
try {
frame.addEventListener("load", t.step_func(() => {
loadCount++;
try {
if (loadCount === 1) {
assert_equals(frame.contentWindow.location.href, new URL(originalPath, location).href, "original page loads");
assert_equals(frame.contentDocument.referrer, expectedReferrer, "referrer is parent frame");
expectedReferrer = referrerPolicyExpectationValue(expected, frame);
}
} finally {
if (loadCount === 1) {
resolveFrameLoaded();
}
}
}));
addEventListener("message", t.step_func(msg => {
const {referrer, documentReferrer, url} = msg.data;
try {
assert_equals(url, aExpectedURL, "refresh page has expected URL");
assert_equals(referrer, expectedReferrer, "referrer header is previous page");
assert_equals(documentReferrer, expectedReferrer, "document referrer is previous page");
} finally {
resolveMessageHandled();
}
}));
frame.src = originalPath;
document.body.appendChild(frame);
await frameLoaded;
await messageHandled;
} finally {
frame.remove();
t.done();
}
}, `${kOriginTypeDescriptions[isSameOrigin]} ${description} with referrer policy "${policy}" refreshes with ${expected} as referrer`)))
}

View File

@ -0,0 +1,16 @@
def main(request, _response):
response_headers = [("Content-Type", "text/html")]
body = "<!doctype html>"
url = request.GET.first(b"url", b"").decode()
if url:
response_headers.append(("Refresh", "0; url=" + url))
body += "Refreshing to %s" % url
else:
body += "Not refreshing"
policy = request.GET.first(b"policy", b"").decode()
response_headers.append(("Referrer-Policy", policy))
return 200, response_headers, body

View File

@ -0,0 +1,14 @@
<!doctype html>
<meta name="referrer" content="{{GET[policy]}}"/>
<span id="info">Refreshing to</span>
<span id=url>{{GET[url]}}</span>
<script>
if (url.textContent !== "") {
const refresh = document.createElement("meta");
refresh.httpEquiv = "refresh";
refresh.content = `0; url=${url.textContent}`;
document.documentElement.appendChild(refresh);
} else {
info.textContent = "Not refreshing.";
}
</script>