Bug 1305237 Expose frameAncestors to webextensions, r?kmag draft
authorShane Caraveo <scaraveo@mozilla.com>
Tue, 10 Oct 2017 09:54:22 -0700
changeset 677723 f90992dc5f0ae1a5871403c3faabd86df44ed66b
parent 677722 39fd326b98c121035a0a46458e259753991ee1dd
child 735196 35191a9edf251b6590d05830cbad17895a03e7c6
push id83777
push usermixedpuppy@gmail.com
push dateTue, 10 Oct 2017 16:54:52 +0000
reviewerskmag
bugs1305237
milestone58.0a1
Bug 1305237 Expose frameAncestors to webextensions, r?kmag MozReview-Commit-ID: 64lIMu6neaD
dom/webidl/ChannelWrapper.webidl
toolkit/components/extensions/test/mochitest/file_simple_sandboxed_frame.html
toolkit/components/extensions/test/mochitest/file_simple_sandboxed_subframe.html
toolkit/components/extensions/test/mochitest/file_simple_xhr_frame2.html
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_webrequest_frameId.html
toolkit/components/extensions/webrequest/ChannelWrapper.cpp
toolkit/components/extensions/webrequest/ChannelWrapper.h
toolkit/modules/addons/WebRequest.jsm
toolkit/modules/addons/WebRequestContent.js
toolkit/modules/tests/browser/browser.ini
toolkit/modules/tests/browser/browser_WebRequest.js
toolkit/modules/tests/browser/browser_WebRequest_ancestors.js
--- 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);
+});