--- a/dom/webidl/ChannelWrapper.webidl
+++ b/dom/webidl/ChannelWrapper.webidl
@@ -281,16 +281,28 @@ interface ChannelWrapper : EventTarget {
/**
* For cross-process requests, the <browser> or <iframe> element to which the
* content loading this request belongs. For requests that don't originate
* from a remote browser, this is null.
*/
[Cached, Pure]
readonly attribute nsISupports? browserElement;
+ /**
+ * Returns an array of objects that combine the url and frameId from the
+ * ancestorPrincipals and ancestorOuterWindowIDs on loadInfo.
+ * The immediate parent is the first entry, the last entry is always the top
+ * level frame. It will be an empty list for toplevel window loads and
+ * non-subdocument resource loads within a toplevel window. For the latter,
+ * originURL will provide information on what window is doing the load. It
+ * will be null if the request is not associated with a window (e.g. XHR with
+ * mozBackgroundRequest = true).
+ */
+ [Cached, Frozen, GetterThrows, Pure]
+ readonly attribute sequence<MozFrameAncestorInfo>? frameAncestors;
/**
* For HTTP requests, returns an array of request headers which will be, or
* have been, sent with this request.
*
* For non-HTTP requests, throws NS_ERROR_UNEXPECTED.
*/
[Throws]
--- a/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html
+++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html
@@ -7,12 +7,17 @@
<body>
<script>
"use strict";
let req = new XMLHttpRequest();
req.open("GET", "/xhr_sandboxed");
req.send();
+
+let sandbox = document.createElement("iframe");
+sandbox.setAttribute("sandbox", "allow-scripts");
+sandbox.setAttribute("src", "file_simple_sandboxed_subframe.html");
+document.documentElement.appendChild(sandbox);
</script>
<img src="file_image_great.png"/>
</body>
</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+</head>
+<body>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html
+++ b/toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html
@@ -13,11 +13,12 @@ let req = new XMLHttpRequest();
req.open("GET", "/xhr_resource_2");
req.send();
let sandbox = document.createElement("iframe");
sandbox.setAttribute("sandbox", "allow-scripts");
sandbox.setAttribute("src", "file_simple_sandboxed_frame.html");
document.documentElement.appendChild(sandbox);
</script>
-<img src="file_image_bad.png#2"/>
+<img src="file_image_redirect.png"/>
+<iframe src="data:text/plain,webRequestTest"/>
</body>
</html>
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -35,16 +35,17 @@ support-files =
file_style_redirect.css
file_script_good.js
file_script_bad.js
file_script_redirect.js
file_script_xhr.js
file_remote_frame.html
file_sample.html
file_simple_sandboxed_frame.html
+ file_simple_sandboxed_subframe.html
file_simple_xhr.html
file_simple_xhr_frame.html
file_simple_xhr_frame2.html
redirect_auto.sjs
redirection.sjs
file_privilege_escalation.html
file_ext_test_api_injection.js
file_permission_xhr.html
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
@@ -40,68 +40,129 @@ let extensionData = {
let expected = {
"file_simple_xhr.html": {
type: "main_frame",
toplevel: true,
},
"file_image_good.png": {
type: "image",
toplevel: true,
+ origin: "file_simple_xhr.html",
},
"example.txt": {
type: "xmlhttprequest",
toplevel: true,
+ origin: "file_simple_xhr.html",
},
+ // sub frames will have the origin and first ancestor is the
+ // parent document
"file_simple_xhr_frame.html": {
type: "sub_frame",
toplevelParent: true,
+ origin: "file_simple_xhr.html",
+ parent: "file_simple_xhr.html",
+ },
+ // a resource in a sub frame will have origin of the subframe,
+ // but the ancestor chain starts with the parent document
+ "xhr_resource": {
+ type: "xmlhttprequest",
+ origin: "file_simple_xhr_frame.html",
+ parent: "file_simple_xhr.html",
},
"file_image_bad.png": {
type: "image",
- },
- "xhr_resource": {
- type: "xmlhttprequest",
+ depth: 2,
+ origin: "file_simple_xhr_frame.html",
+ parent: "file_simple_xhr.html",
},
"file_simple_xhr_frame2.html": {
type: "sub_frame",
+ depth: 2,
+ origin: "file_simple_xhr_frame.html",
+ parent: "file_simple_xhr_frame.html",
},
- "file_image_bad.png#2": {
+ "file_image_redirect.png": {
type: "image",
+ depth: 2,
+ origin: "file_simple_xhr_frame2.html",
+ parent: "file_simple_xhr_frame.html",
},
"xhr_resource_2": {
type: "xmlhttprequest",
+ depth: 2,
+ origin: "file_simple_xhr_frame2.html",
+ parent: "file_simple_xhr_frame.html",
},
- // This is loaded in a sandbox iframe.
+ // Last frame tests content policy frame ancestors.
+ "webRequestTest": {
+ type: "sub_frame",
+ depth: 3,
+ origin: "file_simple_xhr_frame2.html",
+ parent: "file_simple_xhr_frame2.html",
+ },
+ // This is loaded in a sandbox iframe. originUrl is not availabe for that,
+ // and requests within a sandboxed iframe will additionally have an empty
+ // url on their immediate parent/ancestor.
"file_simple_sandboxed_frame.html": {
type: "sub_frame",
+ depth: 3,
+ parent: "file_simple_xhr_frame2.html",
},
"xhr_sandboxed": {
type: "xmlhttprequest",
sandboxed: true,
+ depth: 3,
+ parent: "",
},
"file_image_great.png": {
type: "image",
sandboxed: true,
+ depth: 3,
+ parent: "",
+ },
+ "file_simple_sandboxed_subframe.html": {
+ type: "sub_frame",
+ depth: 4,
+ parent: "",
},
};
function checkDetails(details) {
let url = new URL(details.url);
- let filename = url.pathname.split("/").pop();
+ let filename = url.pathname.split(url.protocol == "data:" ? "," : "/").pop();
let expect = expected[filename];
is(expect.type, details.type, `${details.type} type matches`);
+ if (details.parentFrameId == -1) {
+ is(details.frameAncestors.length, 0, "no ancestors for main_frame requests");
+ } else if (details.parentFrameId == 0) {
+ is(details.frameAncestors.length, 1, "one ancestors for sub_frame requests");
+ } else {
+ ok(details.frameAncestors.length > 1, "have multiple ancestors for deep subframe requests");
+ is(details.frameAncestors.length, expect.depth, "have multiple ancestors for deep subframe requests");
+ }
+ if (details.parentFrameId > -1) {
+ ok(!expect.origin || details.originUrl.includes(expect.origin), "origin url is correct");
+ is(details.frameAncestors[0].frameId, details.parentFrameId, "first ancestor matches request.parentFrameId");
+ ok(details.frameAncestors[0].url.includes(expect.parent), "ancestor parent page correct");
+ is(details.frameAncestors[details.frameAncestors.length - 1].frameId, 0, "last ancestor is always zero");
+ // All our tests should be somewhere within the frame that we set topframe in the query string. That
+ // frame will always be the last ancestor.
+ ok(details.frameAncestors[details.frameAncestors.length - 1].url.includes("topframe=true"), "last ancestor is always topframe");
+ }
if (expect.toplevel) {
is(details.frameId, 0, "expect load at top level");
is(details.parentFrameId, -1, "expect top level frame to have no parent");
} else if (details.type == "sub_frame") {
ok(details.frameId > 0, "expect sub_frame to load into a new frame");
if (expect.toplevelParent) {
is(details.parentFrameId, 0, "expect sub_frame to have top level parent");
+ is(details.frameAncestors.length, 1, "one ancestor for top sub_frame request");
} else {
ok(details.parentFrameId > 0, "expect sub_frame to have parent");
+ ok(details.frameAncestors.length > 1, "sub_frame has ancestors");
}
expect.subframeId = details.frameId;
expect.parentId = details.parentFrameId;
} else if (expect.sandboxed) {
is(details.documentUrl, undefined, "null principal documentUrl for sandboxed request");
} else {
// get the parent frame.
let purl = new URL(details.documentUrl);
@@ -116,28 +177,27 @@ add_task(async function test_webRequest_
// Clear the image cache, since it gets in the way otherwise.
let imgTools = SpecialPowers.Cc["@mozilla.org/image/tools;1"].getService(SpecialPowers.Ci.imgITools);
let cache = imgTools.getImgCacheForDocument(document);
cache.clearCache(false);
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
- let a = addLink(`file_simple_xhr.html?nocache=${Math.random()}`);
+ let a = addLink(`file_simple_xhr.html?topframe=true&nocache=${Math.random()}`);
a.click();
for (let i = 0; i < Object.keys(expected).length; i++) {
checkDetails(await extension.awaitMessage("onBeforeRequest"));
}
let closed = extension.awaitMessage("tab-closed");
extension.sendMessage("close-tab");
await closed;
await extension.unload();
});
-
</script>
</head>
<body>
<div id="test">Sample text</div>
</body>
</html>
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
@@ -564,16 +564,70 @@ ChannelWrapper::ParentWindowId() const
} else {
parentID = loadInfo->GetParentOuterWindowID();
}
return NormalizeWindowID(loadInfo, parentID);
}
return -1;
}
+void
+ChannelWrapper::GetFrameAncestors(dom::Nullable<nsTArray<dom::MozFrameAncestorInfo>>& aFrameAncestors, ErrorResult& aRv) const
+{
+ nsCOMPtr<nsILoadInfo> loadInfo = GetLoadInfo();
+ if (!loadInfo || WindowId(loadInfo) == 0) {
+ aFrameAncestors.SetNull();
+ return;
+ }
+
+ nsresult rv = GetFrameAncestors(loadInfo, aFrameAncestors.SetValue());
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ }
+}
+
+nsresult
+ChannelWrapper::GetFrameAncestors(nsILoadInfo* aLoadInfo, nsTArray<dom::MozFrameAncestorInfo>& aFrameAncestors) const
+{
+ const nsTArray<nsCOMPtr<nsIPrincipal>>& ancestorPrincipals = aLoadInfo->AncestorPrincipals();
+ const nsTArray<uint64_t>& ancestorOuterWindowIDs = aLoadInfo->AncestorOuterWindowIDs();
+ uint32_t size = ancestorPrincipals.Length();
+ MOZ_DIAGNOSTIC_ASSERT(size == ancestorOuterWindowIDs.Length());
+ if (size != ancestorOuterWindowIDs.Length()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ bool subFrame = aLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_SUBDOCUMENT;
+ if (!aFrameAncestors.SetCapacity(subFrame ? size : size + 1, fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // The immediate parent is always the first element in the ancestor arrays, however
+ // SUBDOCUMENTs do not have their immediate parent included, so we inject it here.
+ // This will force wrapper.parentWindowId == wrapper.frameAncestors[0].frameId to
+ // always be true. All ather requests already match this way.
+ if (subFrame) {
+ auto ancestor = aFrameAncestors.AppendElement();
+ GetDocumentURL(ancestor->mUrl);
+ ancestor->mFrameId = ParentWindowId();
+ }
+
+ for (uint32_t i = 0; i < size; ++i) {
+ auto ancestor = aFrameAncestors.AppendElement();
+ nsCOMPtr<nsIURI> uri;
+ MOZ_TRY(ancestorPrincipals[i]->GetURI(getter_AddRefs(uri)));
+ if (!uri) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ MOZ_TRY(uri->GetSpec(ancestor->mUrl));
+ ancestor->mFrameId = NormalizeWindowID(aLoadInfo, ancestorOuterWindowIDs[i]);
+ }
+ return NS_OK;
+}
+
/*****************************************************************************
* Response filtering
*****************************************************************************/
void
ChannelWrapper::RegisterTraceableChannel(const WebExtensionPolicy& aAddon, nsITabParent* aTabParent)
{
mAddonEntries.Put(aAddon.Id(), aTabParent);
@@ -716,17 +770,17 @@ ChannelWrapper::GetStatusLine(nsCString&
already_AddRefed<nsIURI>
ChannelWrapper::FinalURI() const
{
nsCOMPtr<nsIURI> uri;
if (nsCOMPtr<nsIChannel> chan = MaybeChannel()) {
NS_GetFinalChannelURI(chan, getter_AddRefs(uri));
}
- return uri.forget();;
+ return uri.forget();
}
void
ChannelWrapper::GetFinalURL(nsString& aRetVal) const
{
if (HaveChannel()) {
aRetVal = FinalURLInfo().Spec();
}
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.h
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.h
@@ -185,16 +185,18 @@ public:
}
return nullptr;
}
int64_t WindowId() const;
int64_t ParentWindowId() const;
+ void GetFrameAncestors(dom::Nullable<nsTArray<dom::MozFrameAncestorInfo>>& aFrameAncestors, ErrorResult& aRv) const;
+
bool IsSystemLoad() const;
void GetOriginURL(nsCString& aRetVal) const;
void GetDocumentURL(nsCString& aRetVal) const;
already_AddRefed<nsIURI> GetOriginURI() const;
@@ -257,16 +259,18 @@ private:
const URLInfo& FinalURLInfo() const;
const URLInfo* DocumentURLInfo() const;
uint64_t WindowId(nsILoadInfo* aLoadInfo) const;
+ nsresult GetFrameAncestors(nsILoadInfo* aLoadInfo, nsTArray<dom::MozFrameAncestorInfo>& aFrameAncestors) const;
+
static uint64_t GetNextId()
{
static uint64_t sNextId = 1;
return ++sNextId;
}
void CheckEventListeners();
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -166,17 +166,17 @@ class ResponseHeaderChanger extends Head
}
const MAYBE_CACHED_EVENTS = new Set([
"onResponseStarted", "onBeforeRedirect", "onCompleted", "onErrorOccurred",
]);
const OPTIONAL_PROPERTIES = [
"requestHeaders", "responseHeaders", "statusCode", "statusLine", "error", "redirectUrl",
- "requestBody", "scheme", "realm", "isProxy", "challenger", "proxyInfo", "ip",
+ "requestBody", "scheme", "realm", "isProxy", "challenger", "proxyInfo", "ip", "frameAncestors",
];
function serializeRequestData(eventName) {
let data = {
requestId: this.requestId,
url: this.url,
originUrl: this.originUrl,
documentUrl: this.documentUrl,
--- a/toolkit/modules/addons/WebRequestContent.js
+++ b/toolkit/modules/addons/WebRequestContent.js
@@ -111,16 +111,17 @@ var ContentPolicy = {
}
if (!ids.length) {
return Ci.nsIContentPolicy.ACCEPT;
}
let windowId = 0;
let parentWindowId = -1;
+ let frameAncestors = [];
let mm = Services.cpmm;
function getWindowId(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
@@ -145,16 +146,28 @@ var ContentPolicy = {
doc = node;
}
window = doc.defaultView;
}
windowId = getWindowId(window);
if (window.parent !== window) {
parentWindowId = getWindowId(window.parent);
+
+ for (let frame = window.parent; ; frame = frame.parent) {
+ frameAncestors.push({
+ url: frame.document.documentURIObject.spec,
+ frameId: getWindowId(frame),
+ });
+ if (frame === frame.parent) {
+ // Set the last frameId to zero for top level frame.
+ frameAncestors[frameAncestors.length - 1].frameId = 0;
+ break;
+ }
+ }
}
let ir = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor);
try {
// If e10s is disabled, this throws NS_NOINTERFACE for closed tabs.
mm = ir.getInterface(Ci.nsIContentFrameMessageManager);
@@ -165,16 +178,19 @@ var ContentPolicy = {
}
}
let data = {ids,
url,
type: WebRequestCommon.typeForPolicyType(policyType),
windowId,
parentWindowId};
+ if (frameAncestors.length > 0) {
+ data.frameAncestors = frameAncestors;
+ }
if (requestOrigin) {
data.originUrl = requestOrigin.spec;
}
mm.sendAsyncMessage("WebRequest:ShouldLoad", data);
return Ci.nsIContentPolicy.ACCEPT;
},
--- a/toolkit/modules/tests/browser/browser.ini
+++ b/toolkit/modules/tests/browser/browser.ini
@@ -30,14 +30,15 @@ support-files =
[browser_FinderHighlighter.js]
skip-if = debug || os = "linux"
support-files = file_FinderSample.html
[browser_Geometry.js]
[browser_InlineSpellChecker.js]
[browser_WebNavigation.js]
skip-if = true # Superseded by WebExtension tests
[browser_WebRequest.js]
+[browser_WebRequest_ancestors.js]
[browser_WebRequest_cookies.js]
[browser_WebRequest_filtering.js]
[browser_PageMetadata.js]
[browser_PromiseMessage.js]
[browser_RemotePageManager.js]
[browser_Troubleshoot.js]
--- a/toolkit/modules/tests/browser/browser_WebRequest.js
+++ b/toolkit/modules/tests/browser/browser_WebRequest.js
@@ -39,16 +39,21 @@ function onBeforeRequest(details) {
is(details.browser, expected_browser, "correct <browser> element");
checkType(details);
windowIDs.set(details.url, details.windowId);
if (details.url.indexOf("page2") != -1) {
let page1id = windowIDs.get(URL);
ok(details.windowId != page1id, "sub-frame gets its own window ID");
is(details.parentWindowId, page1id, "parent window id is correct");
+
+ is(details.frameAncestors.length, 1, "correctly has only one ancestor");
+ let ancestor = details.frameAncestors[0];
+ ok(ancestor.url.includes("page1"), "parent window url seems correct");
+ is(ancestor.frameId, page1id, "parent window id is correct");
}
}
if (details.url.indexOf("_bad.") != -1) {
return {cancel: true};
}
return undefined;
}
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_WebRequest_ancestors.js
@@ -0,0 +1,54 @@
+"use strict";
+
+var { interfaces: Ci, classes: Cc, utils: Cu, results: Cr } = Components;
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+var {WebRequest} = Cu.import("resource://gre/modules/WebRequest.jsm", {});
+var {PromiseUtils} = Cu.import("resource://gre/modules/PromiseUtils.jsm", {});
+
+add_task(async function test_ancestors_exist() {
+ let deferred = PromiseUtils.defer();
+ function onBeforeRequest(details) {
+ info(`onBeforeRequest ${details.url}`);
+ ok(typeof details.frameAncestors === "object", `ancestors exists [${typeof details.frameAncestors}]`);
+ deferred.resolve();
+ }
+
+ WebRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: new MatchPatternSet(["http://mochi.test/test/*"])}, ["blocking"]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/test/");
+ await deferred.promise;
+ await BrowserTestUtils.removeTab(tab);
+
+ WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
+});
+
+add_task(async function test_ancestors_null() {
+ let deferred = PromiseUtils.defer();
+ function onBeforeRequest(details) {
+ info(`onBeforeRequest ${details.url}`);
+ ok(details.frameAncestors === undefined, "ancestors do not exist");
+ deferred.resolve();
+ }
+
+ WebRequest.onBeforeRequest.addListener(onBeforeRequest, null, ["blocking"]);
+
+ function fetch(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.mozBackgroundRequest = true;
+ xhr.open("GET", url);
+ xhr.onload = () => { resolve(xhr.responseText); };
+ xhr.onerror = () => { reject(xhr.status); };
+ // use a different contextId to avoid auth cache.
+ xhr.setOriginAttributes({userContextId: 1});
+ xhr.send();
+ });
+ }
+
+ await fetch("http://mochi.test:8888/test/");
+ await deferred.promise;
+
+ WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
+});