Bug 1305237 Expose frameAncestors to webextensions, r=bz,kmag
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 27 Sep 2017 07:58:17 -0700
changeset 383299 d0d30a90efa1695eab6ec3f498598fdfd91c1a97
parent 383298 fd1d81b933809ef6d5507e0e279fd926b26bfac0
child 383300 af03af04a37dd18f2eb3465733b38aec988c8275
push id95539
push userkwierso@gmail.com
push dateThu, 28 Sep 2017 00:01:12 +0000
treeherdermozilla-inbound@72de90e66155 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, kmag
bugs1305237
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1305237 Expose frameAncestors to webextensions, r=bz,kmag MozReview-Commit-ID: HpneTIKPoS1
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
@@ -127,16 +127,28 @@ interface ChannelWrapper {
   readonly attribute long long windowId;
 
   [Cached, Constant]
   readonly attribute long long parentWindowId;
 
   [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;
 
   [Throws]
   object getRequestHeaders();
 
   [Throws]
   object getResponseHeaders();
 
   [Throws]
@@ -152,8 +164,22 @@ dictionary MozProxyInfo {
   required ByteString type;
 
   required boolean proxyDNS;
 
   ByteString? username = null;
 
   unsigned long failoverTimeout;
 };
+
+/**
+ * MozFrameAncestorInfo combines loadInfo::AncestorPrincipals with
+ * loadInfo::AncestorOuterWindowIDs for easier access in the WebRequest API.
+ *
+ * url represents the parent of the loading window.
+ * frameId is the outerWindowID for the parent of the loading window.
+ *
+ * For further details see nsILoadInfo.idl and nsIDocument::AncestorPrincipals.
+ */
+dictionary MozFrameAncestorInfo {
+  required ByteString url;
+  required unsigned long long frameId;
+};
--- 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
@@ -471,16 +471,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;
+}
+
 /*****************************************************************************
  * ...
  *****************************************************************************/
 
 MozContentPolicyType
 GetContentPolicyType(uint32_t aType)
 {
   // Note: Please keep this function in sync with the external types in
@@ -594,17 +648,17 @@ ChannelWrapper::GetFinalURI(ErrorResult&
   nsresult rv = NS_ERROR_UNEXPECTED;
   nsCOMPtr<nsIURI> uri;
   if (nsCOMPtr<nsIChannel> chan = MaybeChannel()) {
     rv = NS_GetFinalChannelURI(chan, getter_AddRefs(uri));
   }
   if (NS_FAILED(rv)) {
     aRv.Throw(rv);
   }
-  return uri.forget();;
+  return uri.forget();
 }
 
 void
 ChannelWrapper::GetFinalURL(nsCString& aRetVal, ErrorResult& aRv) const
 {
   nsCOMPtr<nsIURI> uri = GetFinalURI(aRv);
   if (uri) {
     Unused << uri->GetSpec(aRetVal);
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.h
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.h
@@ -148,16 +148,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;
 
@@ -171,17 +173,16 @@ public:
 
   bool GetCanModify(ErrorResult& aRv) const;
 
 
   void GetProxyInfo(dom::Nullable<dom::MozProxyInfo>& aRetVal, ErrorResult& aRv) const;
 
   void GetRemoteAddress(nsCString& aRetVal) const;
 
-
   void GetRequestHeaders(JSContext* cx, JS::MutableHandle<JSObject*> aRetVal, ErrorResult& aRv) const;
 
   void GetResponseHeaders(JSContext* cx, JS::MutableHandle<JSObject*> aRetVal, ErrorResult& aRv) const;
 
   void SetRequestHeader(const nsCString& header, const nsCString& value, ErrorResult& aRv);
 
   void SetResponseHeader(const nsCString& header, const nsCString& value, ErrorResult& aRv);
 
@@ -207,16 +208,18 @@ private:
       aRv.Throw(NS_ERROR_UNEXPECTED);
       return false;
     }
     return true;
   }
 
   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;
   }
 
   const uint64_t mId = GetNextId();
   nsCOMPtr<nsISupports> mParent;
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -183,17 +183,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,
@@ -732,16 +732,23 @@ HttpObserverManager = {
 
       ip: channel.remoteAddress,
 
       proxyInfo: channel.proxyInfo,
 
       serialize: serializeRequestData,
     };
 
+    try {
+      let {frameAncestors} = channel;
+      if (frameAncestors !== null) {
+        data.frameAncestors = frameAncestors;
+      }
+    } catch (e) {}
+
     // force the protocol to be ws again.
     if (data.type == "websocket" && data.url.startsWith("http")) {
       data.url = `ws${data.url.substring(4)}`;
     }
 
     return Object.assign(data, extraData);
   },
 
--- a/toolkit/modules/addons/WebRequestContent.js
+++ b/toolkit/modules/addons/WebRequestContent.js
@@ -113,16 +113,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;
     }
 
@@ -147,16 +148,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);
@@ -167,16 +180,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/*"])}, ["blocking"]);
+
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
+  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/");
+  await deferred.promise;
+
+  WebRequest.onBeforeRequest.removeListener(onBeforeRequest);
+});