Bug 1589407 - Part 2: Ensure that document.referrer sees the same URL as what we send in the Referer HTTP header per the ETP restrictions; r=tnguyen,baku

Differential Revision: https://phabricator.services.mozilla.com/D49773

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ehsan Akhgari 2019-10-26 21:24:57 +00:00
Родитель 9a5d7b4dfa
Коммит c4481cd9ba
12 изменённых файлов: 178 добавлений и 41 удалений

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

@ -1568,17 +1568,18 @@ HttpBaseChannel::GetReferrerInfo(nsIReferrerInfo** aReferrerInfo) {
return NS_OK; return NS_OK;
} }
nsresult HttpBaseChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo, nsresult HttpBaseChannel::SetReferrerInfoInternal(
bool aClone, bool aCompute, nsIReferrerInfo* aReferrerInfo, bool aClone, bool aCompute,
bool aSetOriginal) { bool aRespectBeforeConnect) {
LOG(("HttpBaseChannel::SetReferrerInfo [this=%p aClone(%d) aCompute(%d)]\n", LOG(
("HttpBaseChannel::SetReferrerInfoInternal [this=%p aClone(%d) "
"aCompute(%d)]\n",
this, aClone, aCompute)); this, aClone, aCompute));
ENSURE_CALLED_BEFORE_CONNECT(); if (aRespectBeforeConnect) {
ENSURE_CALLED_BEFORE_CONNECT();
}
mReferrerInfo = aReferrerInfo; mReferrerInfo = aReferrerInfo;
if (aSetOriginal) {
mOriginalReferrerInfo = aReferrerInfo;
}
// clear existing referrer, if any // clear existing referrer, if any
nsresult rv = ClearReferrerHeader(); nsresult rv = ClearReferrerHeader();
@ -1592,9 +1593,6 @@ nsresult HttpBaseChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo,
if (aClone) { if (aClone) {
mReferrerInfo = static_cast<dom::ReferrerInfo*>(aReferrerInfo)->Clone(); mReferrerInfo = static_cast<dom::ReferrerInfo*>(aReferrerInfo)->Clone();
if (aSetOriginal) {
mOriginalReferrerInfo = mReferrerInfo;
}
} }
dom::ReferrerInfo* referrerInfo = dom::ReferrerInfo* referrerInfo =
@ -1624,17 +1622,17 @@ nsresult HttpBaseChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo,
return rv; return rv;
} }
return SetReferrerHeader(spec); return SetReferrerHeader(spec, aRespectBeforeConnect);
} }
NS_IMETHODIMP NS_IMETHODIMP
HttpBaseChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) { HttpBaseChannel::SetReferrerInfo(nsIReferrerInfo* aReferrerInfo) {
return SetReferrerInfo(aReferrerInfo, true, true); return SetReferrerInfoInternal(aReferrerInfo, true, true, true);
} }
NS_IMETHODIMP NS_IMETHODIMP
HttpBaseChannel::SetReferrerInfoWithoutClone(nsIReferrerInfo* aReferrerInfo) { HttpBaseChannel::SetReferrerInfoWithoutClone(nsIReferrerInfo* aReferrerInfo) {
return SetReferrerInfo(aReferrerInfo, false, true); return SetReferrerInfoInternal(aReferrerInfo, false, true, true);
} }
// Return the channel's proxy URI, or if it doesn't exist, the // Return the channel's proxy URI, or if it doesn't exist, the
@ -3141,7 +3139,7 @@ HttpBaseChannel::CloneReplacementChannelConfig(bool aPreserveMethod,
config.privateBrowsing = Some(mPrivateBrowsing); config.privateBrowsing = Some(mPrivateBrowsing);
} }
if (mOriginalReferrerInfo) { if (mReferrerInfo) {
dom::ReferrerPolicy referrerPolicy = dom::ReferrerPolicy::_empty; dom::ReferrerPolicy referrerPolicy = dom::ReferrerPolicy::_empty;
nsAutoCString tRPHeaderCValue; nsAutoCString tRPHeaderCValue;
Unused << GetResponseHeader(NS_LITERAL_CSTRING("referrer-policy"), Unused << GetResponseHeader(NS_LITERAL_CSTRING("referrer-policy"),
@ -3158,11 +3156,11 @@ HttpBaseChannel::CloneReplacementChannelConfig(bool aPreserveMethod,
// changes, we must not use the old computed value, and have to compute // changes, we must not use the old computed value, and have to compute
// again. // again.
nsCOMPtr<nsIReferrerInfo> referrerInfo = nsCOMPtr<nsIReferrerInfo> referrerInfo =
dom::ReferrerInfo::CreateFromOtherAndPolicyOverride( dom::ReferrerInfo::CreateFromOtherAndPolicyOverride(mReferrerInfo,
mOriginalReferrerInfo, referrerPolicy); referrerPolicy);
config.referrerInfo = referrerInfo; config.referrerInfo = referrerInfo;
} else { } else {
config.referrerInfo = mOriginalReferrerInfo; config.referrerInfo = mReferrerInfo;
} }
} }

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

@ -447,8 +447,11 @@ class HttpBaseChannel : public nsHashPropertyBag,
mUploadStreamHasHeaders = hasHeaders; mUploadStreamHasHeaders = hasHeaders;
} }
virtual nsresult SetReferrerHeader(const nsACString& aReferrer) { virtual nsresult SetReferrerHeader(const nsACString& aReferrer,
ENSURE_CALLED_BEFORE_CONNECT(); bool aRespectBeforeConnect = true) {
if (aRespectBeforeConnect) {
ENSURE_CALLED_BEFORE_CONNECT();
}
return mRequestHead.SetHeader(nsHttp::Referer, aReferrer); return mRequestHead.SetHeader(nsHttp::Referer, aReferrer);
} }
@ -470,8 +473,8 @@ class HttpBaseChannel : public nsHashPropertyBag,
// Pass true for aSetOriginal if this is a new referrer and should // Pass true for aSetOriginal if this is a new referrer and should
// overwrite the 'original' value, false if this is a mutation (like // overwrite the 'original' value, false if this is a mutation (like
// stripping the path). // stripping the path).
nsresult SetReferrerInfo(nsIReferrerInfo* aReferrerInfo, bool aClone, nsresult SetReferrerInfoInternal(nsIReferrerInfo* aReferrerInfo, bool aClone,
bool aCompute, bool aSetOriginal = true); bool aCompute, bool aRespectBeforeConnect);
struct ReplacementChannelConfig { struct ReplacementChannelConfig {
ReplacementChannelConfig() = default; ReplacementChannelConfig() = default;
@ -604,10 +607,6 @@ class HttpBaseChannel : public nsHashPropertyBag,
nsCOMPtr<nsIInterfaceRequestor> mCallbacks; nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
nsCOMPtr<nsIProgressEventSink> mProgressSink; nsCOMPtr<nsIProgressEventSink> mProgressSink;
nsCOMPtr<nsIReferrerInfo> mReferrerInfo; nsCOMPtr<nsIReferrerInfo> mReferrerInfo;
// We cache the original value of mReferrerInfo, since
// we trim the referrer to not expose the full path to remote
// usage.
nsCOMPtr<nsIReferrerInfo> mOriginalReferrerInfo;
nsCOMPtr<nsIApplicationCache> mApplicationCache; nsCOMPtr<nsIApplicationCache> mApplicationCache;
nsCOMPtr<nsIURI> mAPIRedirectToURI; nsCOMPtr<nsIURI> mAPIRedirectToURI;
nsCOMPtr<nsIURI> mProxyURI; nsCOMPtr<nsIURI> mProxyURI;

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

@ -3337,6 +3337,19 @@ mozilla::ipc::IPCResult HttpChannelChild::RecvAltDataCacheInputStreamAvailable(
return IPC_OK(); return IPC_OK();
} }
mozilla::ipc::IPCResult
HttpChannelChild::RecvOverrideReferrerInfoDuringBeginConnect(
nsIReferrerInfo* aReferrerInfo) {
// The arguments passed to SetReferrerInfoInternal here should mirror the
// arguments passed in
// nsHttpChannel::ReEvaluateReferrerAfterTrackingStatusIsKnown(), except for
// aRespectBeforeConnect which we pass false here since we're intentionally
// overriding the referrer after BeginConnect().
Unused << SetReferrerInfoInternal(aReferrerInfo, false, true, false);
return IPC_OK();
}
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
// HttpChannelChild::nsIResumableChannel // HttpChannelChild::nsIResumableChannel
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------
@ -3765,11 +3778,14 @@ nsresult HttpChannelChild::AsyncCallImpl(
return rv; return rv;
} }
nsresult HttpChannelChild::SetReferrerHeader(const nsACString& aReferrer) { nsresult HttpChannelChild::SetReferrerHeader(const nsACString& aReferrer,
bool aRespectBeforeConnect) {
// Normally this would be ENSURE_CALLED_BEFORE_CONNECT, but since the // Normally this would be ENSURE_CALLED_BEFORE_CONNECT, but since the
// "connect" is done in the main process, and mRequestObserversCalled is never // "connect" is done in the main process, and mRequestObserversCalled is never
// set in the ChannelChild, before connect basically means before asyncOpen. // set in the ChannelChild, before connect basically means before asyncOpen.
ENSURE_CALLED_BEFORE_ASYNC_OPEN(); if (aRespectBeforeConnect) {
ENSURE_CALLED_BEFORE_ASYNC_OPEN();
}
// remove old referrer if any, loop backwards // remove old referrer if any, loop backwards
for (int i = mClientSetRequestHeaders.Length() - 1; i >= 0; --i) { for (int i = mClientSetRequestHeaders.Length() - 1; i >= 0; --i) {
@ -3779,7 +3795,7 @@ nsresult HttpChannelChild::SetReferrerHeader(const nsACString& aReferrer) {
} }
} }
return HttpBaseChannel::SetReferrerHeader(aReferrer); return HttpBaseChannel::SetReferrerHeader(aReferrer, aRespectBeforeConnect);
} }
class CancelEvent final : public NeckoTargetChannelEvent<HttpChannelChild> { class CancelEvent final : public NeckoTargetChannelEvent<HttpChannelChild> {

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

@ -112,7 +112,8 @@ class HttpChannelChild final : public PHttpChannelChild,
// nsIResumableChannel // nsIResumableChannel
NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override; NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override;
nsresult SetReferrerHeader(const nsACString& aReferrer) override; nsresult SetReferrerHeader(const nsACString& aReferrer,
bool aRespectBeforeConnect) override;
MOZ_MUST_USE bool IsSuspended(); MOZ_MUST_USE bool IsSuspended();
@ -178,6 +179,9 @@ class HttpChannelChild final : public PHttpChannelChild,
mozilla::ipc::IPCResult RecvAltDataCacheInputStreamAvailable( mozilla::ipc::IPCResult RecvAltDataCacheInputStreamAvailable(
const Maybe<IPCStream>& aStream) override; const Maybe<IPCStream>& aStream) override;
mozilla::ipc::IPCResult RecvOverrideReferrerInfoDuringBeginConnect(
nsIReferrerInfo* aReferrerInfo) override;
virtual void ActorDestroy(ActorDestroyReason aWhy) override; virtual void ActorDestroy(ActorDestroyReason aWhy) override;
virtual void DoNotifyListenerCleanup() override; virtual void DoNotifyListenerCleanup() override;

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

@ -485,7 +485,8 @@ bool HttpChannelParent::DoAsyncOpen(
if (docUri) httpChannel->SetDocumentURI(docUri); if (docUri) httpChannel->SetDocumentURI(docUri);
if (aReferrerInfo) { if (aReferrerInfo) {
// Referrer header is computed in child no need to recompute here // Referrer header is computed in child no need to recompute here
rv = httpChannel->SetReferrerInfo(aReferrerInfo, false, false); rv =
httpChannel->SetReferrerInfoInternal(aReferrerInfo, false, false, true);
MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(NS_SUCCEEDED(rv));
} }
@ -917,7 +918,8 @@ mozilla::ipc::IPCResult HttpChannelParent::RecvRedirect2Verify(
MOZ_ASSERT(baseChannel); MOZ_ASSERT(baseChannel);
if (baseChannel) { if (baseChannel) {
// Referrer header is computed in child no need to recompute here // Referrer header is computed in child no need to recompute here
rv = baseChannel->SetReferrerInfo(aReferrerInfo, false, false); rv = baseChannel->SetReferrerInfoInternal(aReferrerInfo, false, false,
true);
MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(NS_SUCCEEDED(rv));
} }
} }
@ -2767,5 +2769,10 @@ nsresult HttpChannelParent::TriggerCrossProcessSwitch(nsIHttpChannel* aChannel,
return NS_OK; return NS_OK;
} }
void HttpChannelParent::OverrideReferrerInfoDuringBeginConnect(
nsIReferrerInfo* aReferrerInfo) {
Unused << SendOverrideReferrerInfoDuringBeginConnect(aReferrerInfo);
}
} // namespace net } // namespace net
} // namespace mozilla } // namespace mozilla

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

@ -136,6 +136,10 @@ class HttpChannelParent final : public nsIInterfaceRequestor,
nsresult TriggerCrossProcessSwitch(nsIHttpChannel* aChannel, nsresult TriggerCrossProcessSwitch(nsIHttpChannel* aChannel,
uint64_t aIdentifier); uint64_t aIdentifier);
// Inform the child actor that our referrer info was modified late during
// BeginConnect.
void OverrideReferrerInfoDuringBeginConnect(nsIReferrerInfo* aReferrerInfo);
protected: protected:
// used to connect redirected-to channel in parent with just created // used to connect redirected-to channel in parent with just created
// ChildChannel. Used during redirects. // ChildChannel. Used during redirects.

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

@ -188,6 +188,8 @@ child:
async AltDataCacheInputStreamAvailable(IPCStream? stream); async AltDataCacheInputStreamAvailable(IPCStream? stream);
async OverrideReferrerInfoDuringBeginConnect(nsIReferrerInfo referrerInfo);
both: both:
// After receiving this message, the parent also calls // After receiving this message, the parent also calls
// SendFinishInterceptedRedirect, and makes sure not to send any more messages // SendFinishInterceptedRedirect, and makes sure not to send any more messages

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

@ -10434,9 +10434,17 @@ void nsHttpChannel::ReEvaluateReferrerAfterTrackingStatusIsKnown() {
isPrivate)) { isPrivate)) {
nsCOMPtr<nsIReferrerInfo> newReferrerInfo = nsCOMPtr<nsIReferrerInfo> newReferrerInfo =
referrerInfo->CloneWithNewPolicy(ReferrerPolicy::_empty); referrerInfo->CloneWithNewPolicy(ReferrerPolicy::_empty);
// Pass false for the 3rd bool to not overwrite the original // The arguments passed to SetReferrerInfoInternal here should mirror
// referrer for these referrer policy mutations. // the arguments passed in
SetReferrerInfo(newReferrerInfo, false, true, false); // HttpChannelChild::RecvOverrideReferrerInfoDuringBeginConnect().
SetReferrerInfoInternal(newReferrerInfo, false, true, true);
nsCOMPtr<nsIParentChannel> parentChannel;
NS_QueryNotificationCallbacks(this, parentChannel);
RefPtr<HttpChannelParent> httpParent = do_QueryObject(parentChannel);
if (httpParent) {
httpParent->OverrideReferrerInfoDuringBeginConnect(newReferrerInfo);
}
} }
} }
} }

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

@ -65,7 +65,7 @@ function run_test() {
var chan = make_channel(URL + redirects[0]); var chan = make_channel(URL + redirects[0]);
var uri = NetUtil.newURI("http://test.com"); var uri = NetUtil.newURI("http://test.com");
httpChan = chan.QueryInterface(Ci.nsIHttpChannel); var httpChan = chan.QueryInterface(Ci.nsIHttpChannel);
httpChan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); httpChan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri);
chan.asyncOpen(new ChannelListener(finish_test, null)); chan.asyncOpen(new ChannelListener(finish_test, null));
do_test_pending(); do_test_pending();

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

@ -23,7 +23,7 @@ async function testOnWindowBody(win, expectedReferrer, rp) {
await promiseTabLoadEvent(tab, TEST_TOP_PAGE); await promiseTabLoadEvent(tab, TEST_TOP_PAGE);
info("Loading tracking scripts and tracking images"); info("Loading tracking scripts and tracking images");
await ContentTask.spawn(b, { rp }, async function({ rp }) { let referrer = await ContentTask.spawn(b, { rp }, async function({ rp }) {
{ {
let src = content.document.createElement("script"); let src = content.document.createElement("script");
let p = new content.Promise(resolve => { let p = new content.Promise(resolve => {
@ -51,8 +51,32 @@ async function testOnWindowBody(win, expectedReferrer, rp) {
"https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=image"; "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=image";
await p; await p;
} }
{
let iframe = content.document.createElement("iframe");
let p = new content.Promise(resolve => {
iframe.onload = resolve;
});
content.document.body.appendChild(iframe);
if (rp) {
iframe.referrerPolicy = rp;
}
iframe.src =
"https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?what=iframe";
await p;
p = new content.Promise(resolve => {
content.onmessage = event => {
resolve(event.data);
};
});
iframe.contentWindow.postMessage("ping", "*");
return p;
}
}); });
is(referrer, expectedReferrer, "The correct referrer must be read from DOM");
await fetch( await fetch(
"https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=script" "https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=script"
) )
@ -68,6 +92,14 @@ async function testOnWindowBody(win, expectedReferrer, rp) {
.then(text => { .then(text => {
is(text, expectedReferrer, "We sent the correct Referer header"); is(text, expectedReferrer, "We sent the correct Referer header");
}); });
await fetch(
"https://tracking.example.org/browser/toolkit/components/antitracking/test/browser/referrer.sjs?result&what=iframe"
)
.then(r => r.text())
.then(text => {
is(text, expectedReferrer, "We sent the correct Referer header");
});
} }
async function closeAWindow(win) { async function closeAWindow(win) {

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

@ -144,7 +144,35 @@ AntiTracking._createTask({
ok(false, "No query parameters should be found"); ok(false, "No query parameters should be found");
} }
}, },
extraPrefs: null, extraPrefs: [["network.http.referer.defaultPolicy.trackers", 3]],
expectedBlockingNotifications: 0,
runInPrivateWindow: false,
iframeSandbox: null,
accessRemoval: null,
callbackAfterRemoval: null,
topPage: TOP_PAGE_WITHOUT_TRACKING_IDENTIFIER,
});
AntiTracking._createTask({
name:
"Test that we do not downgrade document.referrer when it does not contain a tracking identifier even though it gets downgraded to origin only due to the default referrer policy",
cookieBehavior: BEHAVIOR_REJECT_TRACKER,
blockingByContentBlockingRTUI: true,
allowList: false,
callback: async _ => {
let ref = new URL(document.referrer);
is(
ref.hostname,
"sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
"Hostname shouldn't be stripped"
);
is(ref.pathname.length, 1, "Path must be trimmed");
// eslint-disable-next-line no-unused-vars
for (let entry of ref.searchParams.entries()) {
ok(false, "No query parameters should be found");
}
},
extraPrefs: [["network.http.referer.defaultPolicy.trackers", 2]],
expectedBlockingNotifications: 0, expectedBlockingNotifications: 0,
runInPrivateWindow: false, runInPrivateWindow: false,
iframeSandbox: null, iframeSandbox: null,
@ -172,7 +200,35 @@ AntiTracking._createTask({
ok(false, "No query parameters should be found"); ok(false, "No query parameters should be found");
} }
}, },
extraPrefs: null, extraPrefs: [["network.http.referer.defaultPolicy.trackers", 3]],
expectedBlockingNotifications: 0,
runInPrivateWindow: false,
iframeSandbox: null,
accessRemoval: null,
callbackAfterRemoval: null,
topPage: TOP_PAGE_WITH_TRACKING_IDENTIFIER,
});
AntiTracking._createTask({
name:
"Test that we don't downgrade document.referrer when it contains a tracking identifier if it gets downgraded to origin only due to the default referrer policy because the tracking identifier wouldn't be present in the referrer any more",
cookieBehavior: BEHAVIOR_REJECT_TRACKER,
blockingByContentBlockingRTUI: true,
allowList: false,
callback: async _ => {
let ref = new URL(document.referrer);
is(
ref.hostname,
"sub1.xn--hxajbheg2az3al.xn--jxalpdlp",
"Hostname shouldn't be stripped"
);
is(ref.pathname.length, 1, "Path must be trimmed");
// eslint-disable-next-line no-unused-vars
for (let entry of ref.searchParams.entries()) {
ok(false, "No query parameters should be found");
}
},
extraPrefs: [["network.http.referer.defaultPolicy.trackers", 2]],
expectedBlockingNotifications: 0, expectedBlockingNotifications: 0,
runInPrivateWindow: false, runInPrivateWindow: false,
iframeSandbox: null, iframeSandbox: null,

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

@ -3,10 +3,18 @@
const IMAGE = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" + const IMAGE = atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAA" +
"ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="); "ACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=");
const IFRAME = "<!DOCTYPE html>\n" +
"<script>\n" +
"onmessage = event => {\n" +
"parent.postMessage(document.referrer, '*');\n" +
"};\n" +
"</script>";
function handleRequest(aRequest, aResponse) { function handleRequest(aRequest, aResponse) {
aResponse.setStatusLine(aRequest.httpVersion, 200); aResponse.setStatusLine(aRequest.httpVersion, 200);
let key = aRequest.queryString.includes("what=script") ? "script" : "image"; let key = aRequest.queryString.includes("what=script") ? "script" :
(aRequest.queryString.includes("what=image") ? "image" : "iframe");
if (aRequest.queryString.includes("result")) { if (aRequest.queryString.includes("result")) {
aResponse.write(getState(key)); aResponse.write(getState(key));
@ -22,8 +30,11 @@ function handleRequest(aRequest, aResponse) {
if (key == "script") { if (key == "script") {
aResponse.setHeader("Content-Type", "text/javascript", false); aResponse.setHeader("Content-Type", "text/javascript", false);
aResponse.write("42;"); aResponse.write("42;");
} else { } else if (key == "image") {
aResponse.setHeader("Content-Type", "image/png", false); aResponse.setHeader("Content-Type", "image/png", false);
aResponse.write(IMAGE); aResponse.write(IMAGE);
} else {
aResponse.setHeader("Content-Type", "text/html", false);
aResponse.write(IFRAME);
} }
} }