Bug 1425197 - Add additional checks of the target URL in the inspectedWindow devtools actor. r=kmag, r=ochameau a=jcristau
authorLuca Greco <lgreco@mozilla.com>
Wed, 04 Apr 2018 19:59:18 +0200
changeset 463042 7a7905efa6eb152c4e0b043abf91b0b88149d0a6
parent 463041 da82875f3867af7e5150bd20c7662cd5ad1da718
child 463043 da276e11c366eaa9470af08cc12428a6298928af
push id1683
push usersfraser@mozilla.com
push dateThu, 26 Apr 2018 16:43:40 +0000
treeherdermozilla-release@5af6cb21869d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag, ochameau, jcristau
bugs1425197
milestone60.0
Bug 1425197 - Add additional checks of the target URL in the inspectedWindow devtools actor. r=kmag, r=ochameau a=jcristau
browser/components/extensions/ext-devtools-inspectedWindow.js
browser/components/extensions/ext-devtools-panels.js
devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
devtools/server/actors/webextension-inspected-window.js
devtools/server/tests/browser/browser_webextension_inspected_window.js
devtools/server/tests/browser/inspectedwindow-reload-target.sjs
dom/chrome-webidl/WebExtensionPolicy.webidl
toolkit/components/extensions/WebExtensionPolicy.h
--- a/browser/components/extensions/ext-devtools-inspectedWindow.js
+++ b/browser/components/extensions/ext-devtools-inspectedWindow.js
@@ -10,18 +10,19 @@ var {
   SpreadArgs,
 } = ExtensionCommon;
 
 this.devtools_inspectedWindow = class extends ExtensionAPI {
   getAPI(context) {
     // Lazily retrieved inspectedWindow actor front per child context.
     let waitForInspectedWindowFront;
 
-    // TODO(rpl): retrive a more detailed callerInfo object, like the filename and
-    // lineNumber of the actual extension called, in the child process.
+    // TODO - Bug 1448878: retrive a more detailed callerInfo object,
+    // like the filename and lineNumber of the actual extension called
+    // in the child process.
     const callerInfo = {
       addonId: context.extension.id,
       url: context.extension.baseURI.spec,
     };
 
     return {
       devtools: {
         inspectedWindow: {
--- a/browser/components/extensions/ext-devtools-panels.js
+++ b/browser/components/extensions/ext-devtools-panels.js
@@ -504,18 +504,19 @@ class ParentDevToolsInspectorSidebar {
 const sidebarsById = new Map();
 
 this.devtools_panels = class extends ExtensionAPI {
   getAPI(context) {
     // Lazily retrieved inspectedWindow actor front per child context
     // (used by Sidebar.setExpression).
     let waitForInspectedWindowFront;
 
-    // TODO(rpl): retrive a more detailed callerInfo object, like the filename and
-    // lineNumber of the actual extension called, in the child process.
+    // TODO - Bug 1448878: retrive a more detailed callerInfo object,
+    // like the filename and lineNumber of the actual extension called
+    // in the child process.
     const callerInfo = {
       addonId: context.extension.id,
       url: context.extension.baseURI.spec,
     };
 
     // An incremental "per context" id used in the generated devtools panel id.
     let nextPanelId = 0;
 
--- a/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
+++ b/devtools/client/inspector/extensions/test/browser_inspector_extension_sidebar.js
@@ -8,28 +8,46 @@ ChromeUtils.defineModuleGetter(this, "Co
                                "resource://testing-common/ContentTaskUtils.jsm");
 
 loader.lazyGetter(this, "WebExtensionInspectedWindowFront", () => {
   return require(
     "devtools/shared/fronts/webextension-inspected-window"
   ).WebExtensionInspectedWindowFront;
 }, true);
 
-const FAKE_CALLER_INFO = {
-  url: "moz-extension://fake-webextension-uuid/fake-caller-script.js",
-  lineNumber: 1,
-  addonId: "fake-webextension-uuid",
-};
+ChromeUtils.defineModuleGetter(this, "ExtensionParent",
+                               "resource://gre/modules/ExtensionParent.jsm");
+
+const {WebExtensionPolicy} = ExtensionParent;
+
 const SIDEBAR_ID = "an-extension-sidebar";
 const SIDEBAR_TITLE = "Sidebar Title";
 
+let extension;
+let fakeExtCallerInfo;
+
 let toolbox;
 let inspector;
 
 add_task(async function setupExtensionSidebar() {
+  extension = ExtensionTestUtils.loadExtension({
+    background() {
+      // This is just an empty extension used to ensure that the caller extension uuid
+      // actually exists.
+    }
+  });
+
+  await extension.startup();
+
+  fakeExtCallerInfo = {
+    url: WebExtensionPolicy.getByID(extension.id).getURL("fake-caller-script.js"),
+    lineNumber: 1,
+    addonId: extension.id,
+  };
+
   const res = await openInspectorForURL("about:blank");
   inspector = res.inspector;
   toolbox = res.toolbox;
 
   const onceSidebarCreated = toolbox.once(`extension-sidebar-created-${SIDEBAR_ID}`);
   toolbox.registerInspectorExtensionSidebar(SIDEBAR_ID, {title: SIDEBAR_TITLE});
 
   const sidebar = await onceSidebarCreated;
@@ -112,17 +130,17 @@ add_task(async function testSidebarSetOb
   let expression = `
     var obj = Object.create(null);
     obj.prop1 = 123;
     obj[Symbol('sym1')] = 456;
     obj.cyclic = obj;
     obj;
   `;
 
-  let evalResult = await inspectedWindowFront.eval(FAKE_CALLER_INFO, expression, {
+  let evalResult = await inspectedWindowFront.eval(fakeExtCallerInfo, expression, {
     evalResultAsGrip: true,
     toolboxConsoleActorID: toolbox.target.form.consoleActor
   });
 
   sidebar.setObjectValueGrip(evalResult.valueGrip, "Expected Root Title");
 
   // Wait the ObjectInspector component to be rendered and test its content.
   await testSetExpressionSidebarPanel(sidebarPanelContent, {
@@ -149,17 +167,17 @@ add_task(async function testSidebarDOMNo
     toolbox.target.client, toolbox.target.form
   );
 
   const sidebar = inspector.getPanel(SIDEBAR_ID);
   const sidebarPanelContent = inspector.sidebar.getTabPanel(SIDEBAR_ID);
 
   let expression = "({ body: document.body })";
 
-  let evalResult = await inspectedWindowFront.eval(FAKE_CALLER_INFO, expression, {
+  let evalResult = await inspectedWindowFront.eval(fakeExtCallerInfo, expression, {
     evalResultAsGrip: true,
     toolboxConsoleActorID: toolbox.target.form.consoleActor
   });
 
   sidebar.setObjectValueGrip(evalResult.valueGrip);
 
   // Wait the DOM node to be rendered inside the component.
   await waitForObjectInspector(sidebarPanelContent, "node");
@@ -231,11 +249,14 @@ add_task(async function teardownExtensio
 
   let inspectorStoreState = inspector.store.getState();
 
   Assert.deepEqual(inspectorStoreState.extensionsSidebar, {},
                    "The extensions sidebar Redux store data has been cleared");
 
   await toolbox.destroy();
 
+  await extension.unload();
+
   toolbox = null;
   inspector = null;
+  extension = null;
 });
--- a/devtools/server/actors/webextension-inspected-window.js
+++ b/devtools/server/actors/webextension-inspected-window.js
@@ -1,31 +1,105 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const protocol = require("devtools/shared/protocol");
 
-const {Ci, Cu, Cr} = require("chrome");
+const {Cc, Ci, Cu, Cr} = require("chrome");
 
 const {DebuggerServer} = require("devtools/server/main");
 const Services = require("Services");
 
 loader.lazyGetter(this, "NodeActor", () => require("devtools/server/actors/inspector/node").NodeActor, true);
 
 const {
   XPCOMUtils,
 } = require("resource://gre/modules/XPCOMUtils.jsm");
 
 const {
   webExtensionInspectedWindowSpec,
 } = require("devtools/shared/specs/webextension-inspected-window");
 
+const {WebExtensionPolicy} = Cu.getGlobalForObject(XPCOMUtils);
+
+// A weak set of the documents for which a warning message has been
+// already logged (so that we don't keep emitting the same warning if an
+// extension keeps calling the devtools.inspectedWindow.eval API method
+// when it fails to retrieve a result, but we do log the warning message
+// if the user reloads the window):
+//
+// WeakSet<Document>
+const deniedWarningDocuments = new WeakSet();
+
+function isSystemPrincipalWindow(window) {
+  return window.document.nodePrincipal.isSystemPrincipal;
+}
+
+// Create the exceptionInfo property in the format expected by a
+// WebExtension inspectedWindow.eval API calls.
+function createExceptionInfoResult(props) {
+  return {
+    exceptionInfo: {
+      isError: true,
+      code: "E_PROTOCOLERROR",
+      description: "Unknown Inspector protocol error",
+
+      // Apply the passed properties.
+      ...props,
+    }
+  };
+}
+
+// Show a warning message in the webconsole when an extension
+// eval request has been denied, so that the user knows about it
+// even if the extension doesn't report the error itself.
+function logAccessDeniedWarning(window, callerInfo, extensionPolicy) {
+  // Do not log the same warning multiple times for the same document.
+  if (deniedWarningDocuments.has(window.document)) {
+    return;
+  }
+
+  deniedWarningDocuments.add(window.document);
+
+  const {name} = extensionPolicy;
+
+  // System principals have a null nodePrincipal.URI and so we use
+  // the url from window.location.href.
+  const reportedURI = isSystemPrincipalWindow(window) ?
+    Services.io.newURI(window.location.href) : window.document.nodePrincipal.URI;
+
+  const error = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+
+  const msg = `The extension "${name}" is not allowed to access ${reportedURI.spec}`;
+
+  const innerWindowId = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+
+  const errorFlag = 0;
+
+  let {url, lineNumber} = callerInfo;
+
+  let callerURI = callerInfo.url && Services.io.newURI(callerInfo.url);
+
+  // callerInfo.url is not the full path to the file that called the WebExtensions
+  // API yet (Bug 1448878), and so we associate the error to the url of the extension
+  // manifest.json file as a fallback.
+  if (callerURI.filePath === "/") {
+    url = extensionPolicy.getURL("/manifest.json");
+    lineNumber = null;
+  }
+
+  error.initWithWindowID(msg, url, lineNumber, 0, 0, errorFlag, "webExtensions",
+                         innerWindowId);
+  Services.console.logMessage(error);
+}
+
 function CustomizedReload(params) {
   this.docShell = params.tabActor.window
                         .QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDocShell);
   this.docShell.QueryInterface(Ci.nsIWebProgress);
 
   this.inspectedWindowEval = params.inspectedWindowEval;
   this.callerInfo = params.callerInfo;
@@ -209,21 +283,16 @@ var WebExtensionInspectedWindowActor = p
       }
 
       if (this._dbg) {
         this._dbg.enabled = false;
         delete this._dbg;
       }
     },
 
-    isSystemPrincipal(window) {
-      const principal = window.document.nodePrincipal;
-      return Services.scriptSecurityManager.isSystemPrincipal(principal);
-    },
-
     get dbg() {
       if (this._dbg) {
         return this._dbg;
       }
 
       this._dbg = this.tabActor.makeDebugger();
       return this._dbg;
     },
@@ -304,17 +373,17 @@ var WebExtensionInspectedWindowActor = p
      * @param {string|undefined}        options.userAgent
      *   customize the userAgent during the page reload.
      * @param {string|undefined}        options.injectedScript
      *   evaluate the provided javascript code in the top level and every sub-frame
      *   created during the page reload, before any other script in the page has been
      *   executed.
      */
     reload(callerInfo, {ignoreCache, userAgent, injectedScript}) {
-      if (this.isSystemPrincipal(this.window)) {
+      if (isSystemPrincipalWindow(this.window)) {
         console.error("Ignored inspectedWindow.reload on system principal target for " +
                       `${callerInfo.url}:${callerInfo.lineNumber}`);
         return {};
       }
 
       const delayedReload = () => {
         // This won't work while the browser is shutting down and we don't really
         // care.
@@ -404,58 +473,99 @@ var WebExtensionInspectedWindowActor = p
      *   NOTE: this parameter is not part of the RDP protocol exposed by this actor, when
      *   it is called over the remote debugging protocol the target window is always
      *   `tabActor.window`.
      */
     eval(callerInfo, expression, options, customTargetWindow) {
       const window = customTargetWindow || this.window;
       options = options || {};
 
+      const extensionPolicy = WebExtensionPolicy.getByID(callerInfo.addonId);
+
+      if (!extensionPolicy) {
+        return createExceptionInfoResult({
+          description: "Inspector protocol error: %s %s",
+          details: [
+            "Caller extension not found for",
+            callerInfo.url
+          ],
+        });
+      }
+
       if (!window) {
-        return {
-          exceptionInfo: {
-            isError: true,
-            code: "E_PROTOCOLERROR",
-            description: "Inspector protocol error: %s",
-            details: [
-              "The target window is not defined. inspectedWindow.eval not executed.",
-            ],
-          },
-        };
+        return createExceptionInfoResult({
+          description: "Inspector protocol error: %s",
+          details: [
+            "The target window is not defined. inspectedWindow.eval not executed.",
+          ],
+        });
       }
 
-      if (this.isSystemPrincipal(window)) {
-        // On denied JS evaluation, report it using the same data format
+      // Log the error for the user to know that the extension request has been denied
+      // (the extension may not warn the user at all).
+      const logEvalDenied = () => {
+        logAccessDeniedWarning(window, callerInfo, extensionPolicy);
+      };
+
+      if (isSystemPrincipalWindow(window)) {
+        logEvalDenied();
+
+        // On denied JS evaluation, report it to the extension using the same data format
         // used in the corresponding chrome API method to report issues that are
         // not exceptions raised in the evaluated javascript code.
-        return {
-          exceptionInfo: {
-            isError: true,
-            code: "E_PROTOCOLERROR",
-            description: "Inspector protocol error: %s",
-            details: [
-              "This target has a system principal. inspectedWindow.eval denied.",
-            ],
-          },
-        };
+        return createExceptionInfoResult({
+          description: "Inspector protocol error: %s",
+          details: [
+            "This target has a system principal. inspectedWindow.eval denied.",
+          ],
+        });
+      }
+
+      let docPrincipalURI = window.document.nodePrincipal.URI;
+
+      // Deny on document principals listed as restricted or
+      // related to the about: pages (only about:blank and about:srcdoc are
+      // allowed and their are expected to not have their about URI associated
+      // to the principal).
+      if (WebExtensionPolicy.isRestrictedURI(docPrincipalURI) ||
+          docPrincipalURI.schemeIs("about")) {
+        logEvalDenied();
+
+        return createExceptionInfoResult({
+          description: "Inspector protocol error: %s %s",
+          details: [
+            "This extension is not allowed on the current inspected window origin",
+            docPrincipalURI.spec,
+          ],
+        });
+      }
+
+      const windowAddonId = window.document.nodePrincipal.addonId;
+
+      if (windowAddonId && extensionPolicy.id !== windowAddonId) {
+        logEvalDenied();
+
+        return createExceptionInfoResult({
+          description: "Inspector protocol error: %s on %s",
+          details: [
+            "This extension is not allowed to access this extension page.",
+            window.document.location.origin,
+          ],
+        });
       }
 
       // Raise an error on the unsupported options.
       if (options.frameURL || options.contextSecurityOrigin ||
           options.useContentScriptContext) {
-        return {
-          exceptionInfo: {
-            isError: true,
-            code: "E_PROTOCOLERROR",
-            description: "Inspector protocol error: %s",
-            details: [
-              "The inspectedWindow.eval options are currently not supported",
-            ],
-          },
-        };
+        return createExceptionInfoResult({
+          description: "Inspector protocol error: %s",
+          details: [
+            "The inspectedWindow.eval options are currently not supported",
+          ],
+        });
       }
 
       const dbgWindow = this.dbg.makeGlobalObjectReference(window);
 
       let evalCalledFrom = callerInfo.url;
       if (callerInfo.lineNumber) {
         evalCalledFrom += `:${callerInfo.lineNumber}`;
       }
@@ -500,27 +610,23 @@ var WebExtensionInspectedWindowActor = p
       }
 
       if (evalResult) {
         try {
           // Return the evalResult as a grip (used by the WebExtensions
           // devtools inspector's sidebar.setExpression API method).
           if (options.evalResultAsGrip) {
             if (!options.toolboxConsoleActorID) {
-              return {
-                exceptionInfo: {
-                  isError: true,
-                  code: "E_PROTOCOLERROR",
-                  description: "Inspector protocol error: %s - %s",
-                  details: [
-                    "Unexpected invalid sidebar panel expression request",
-                    "missing toolboxConsoleActorID",
-                  ],
-                },
-              };
+              return createExceptionInfoResult({
+                description: "Inspector protocol error: %s - %s",
+                details: [
+                  "Unexpected invalid sidebar panel expression request",
+                  "missing toolboxConsoleActorID",
+                ],
+              });
             }
 
             let consoleActor = DebuggerServer.searchAllConnectionsForActor(
               options.toolboxConsoleActorID
             );
 
             return {valueGrip: consoleActor.createValueGrip(evalResult)};
           }
@@ -528,26 +634,22 @@ var WebExtensionInspectedWindowActor = p
           if (evalResult && typeof evalResult === "object") {
             evalResult = evalResult.unsafeDereference();
           }
           evalResult = JSON.parse(JSON.stringify(evalResult));
         } catch (err) {
           // The evaluation result cannot be sent over the RDP Protocol,
           // report it as with the same data format used in the corresponding
           // chrome API method.
-          return {
-            exceptionInfo: {
-              isError: true,
-              code: "E_PROTOCOLERROR",
-              description: "Inspector protocol error: %s",
-              details: [
-                String(err),
-              ],
-            },
-          };
+          return createExceptionInfoResult({
+            description: "Inspector protocol error: %s",
+            details: [
+              String(err),
+            ],
+          });
         }
       }
 
       return {value: evalResult};
     }
   }
 );
 
--- a/devtools/server/tests/browser/browser_webextension_inspected_window.js
+++ b/devtools/server/tests/browser/browser_webextension_inspected_window.js
@@ -1,50 +1,66 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
+ChromeUtils.defineModuleGetter(this, "ExtensionParent",
+                               "resource://gre/modules/ExtensionParent.jsm");
+
+const {WebExtensionPolicy} = ExtensionParent;
+
 const {
   WebExtensionInspectedWindowFront
 } = require("devtools/shared/fronts/webextension-inspected-window");
 
 const TEST_RELOAD_URL = `${MAIN_DOMAIN}/inspectedwindow-reload-target.sjs`;
 
-const FAKE_CALLER_INFO = {
-  url: "moz-extension://fake-webextension-uuid/fake-caller-script.js",
-  lineNumber: 1,
-  addonId: "fake-webextension-uuid",
-};
+async function setup(pageUrl) {
+  const extension = ExtensionTestUtils.loadExtension({
+    background() {
+      // This is just an empty extension used to ensure that the caller extension uuid
+      // actually exists.
+    }
+  });
 
-async function setup(pageUrl) {
+  await extension.startup();
+
+  const fakeExtCallerInfo = {
+    url: WebExtensionPolicy.getByID(extension.id).getURL("fake-caller-script.js"),
+    lineNumber: 1,
+    addonId: extension.id,
+  };
+
   await addTab(pageUrl);
   initDebuggerServer();
 
   const client = new DebuggerClient(DebuggerServer.connectPipe());
   const form = await connectDebuggerClient(client);
 
   const [, tabClient] = await client.attachTab(form.actor);
 
   const [, consoleClient] = await client.attachConsole(form.consoleActor, []);
 
   const inspectedWindowFront = new WebExtensionInspectedWindowFront(client, form);
 
   return {
     client, form,
     tabClient, consoleClient,
     inspectedWindowFront,
+    extension, fakeExtCallerInfo,
   };
 }
 
-async function teardown({client}) {
+async function teardown({client, extension}) {
   await client.close();
   DebuggerServer.destroy();
   gBrowser.removeCurrentTab();
+  await extension.unload();
 }
 
 function waitForNextTabNavigated(client) {
   return new Promise(resolve => {
     client.addListener("tabNavigated", function tabNavigatedListener(evt, pkt) {
       if (pkt.state == "stop" && !pkt.isFrameSwitching) {
         client.removeListener("tabNavigated", tabNavigatedListener);
         resolve();
@@ -82,43 +98,52 @@ function collectEvalResults() {
     }
     const iframe = iframeDoc.querySelector("iframe");
     iframeDoc = iframe ? iframe.contentDocument : null;
   }
   return JSON.stringify(results);
 }
 
 add_task(async function test_successfull_inspectedWindowEval_result() {
-  const {client, inspectedWindowFront} = await setup(MAIN_DOMAIN);
-  const result = await inspectedWindowFront.eval(FAKE_CALLER_INFO, "window.location", {});
+  const {
+    client, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
+  } = await setup(MAIN_DOMAIN);
+
+  const result = await inspectedWindowFront.eval(fakeExtCallerInfo,
+                                                 "window.location", {});
 
   ok(result.value, "Got a result from inspectedWindow eval");
   is(result.value.href, MAIN_DOMAIN,
      "Got the expected window.location.href property value");
   is(result.value.protocol, "http:",
      "Got the expected window.location.protocol property value");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_successfull_inspectedWindowEval_resultAsGrip() {
-  const {client, inspectedWindowFront, form} = await setup(MAIN_DOMAIN);
-  let result = await inspectedWindowFront.eval(FAKE_CALLER_INFO, "window", {
+  const {
+    client, inspectedWindowFront, form,
+    extension, fakeExtCallerInfo,
+  } = await setup(MAIN_DOMAIN);
+
+  let result = await inspectedWindowFront.eval(fakeExtCallerInfo, "window", {
     evalResultAsGrip: true,
     toolboxConsoleActorID: form.consoleActor
   });
 
   ok(result.valueGrip, "Got a result from inspectedWindow eval");
   ok(result.valueGrip.actor, "Got a object actor as expected");
   is(result.valueGrip.type, "object", "Got a value grip of type object");
   is(result.valueGrip.class, "Window", "Got a value grip which is instanceof Location");
 
   // Test invalid evalResultAsGrip request.
   result = await inspectedWindowFront.eval(
-    FAKE_CALLER_INFO, "window", {evalResultAsGrip: true}
+    fakeExtCallerInfo, "window", {evalResultAsGrip: true}
   );
 
   ok(!result.value && !result.valueGrip,
      "Got a null result from the invalid inspectedWindow eval call");
   ok(result.exceptionInfo.isError, "Got an API Error result from inspectedWindow eval");
   ok(!result.exceptionInfo.isException, "An error isException is false as expected");
   is(result.exceptionInfo.code, "E_PROTOCOLERROR",
      "Got the expected 'code' property in the error result");
@@ -128,244 +153,261 @@ add_task(async function test_successfull
      "The 'details' array property should contains 1 element");
   is(result.exceptionInfo.details[0],
      "Unexpected invalid sidebar panel expression request",
      "Got the expected content in the error results's details");
   is(result.exceptionInfo.details[1],
      "missing toolboxConsoleActorID",
      "Got the expected content in the error results's details");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_error_inspectedWindowEval_result() {
-  const {client, inspectedWindowFront} = await setup(MAIN_DOMAIN);
-  const result = await inspectedWindowFront.eval(FAKE_CALLER_INFO, "window", {});
+  const {
+    client, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
+  } = await setup(MAIN_DOMAIN);
+
+  const result = await inspectedWindowFront.eval(fakeExtCallerInfo, "window", {});
 
   ok(!result.value, "Got a null result from inspectedWindow eval");
   ok(result.exceptionInfo.isError, "Got an API Error result from inspectedWindow eval");
   ok(!result.exceptionInfo.isException, "An error isException is false as expected");
   is(result.exceptionInfo.code, "E_PROTOCOLERROR",
      "Got the expected 'code' property in the error result");
   is(result.exceptionInfo.description, "Inspector protocol error: %s",
      "Got the expected 'description' property in the error result");
   is(result.exceptionInfo.details.length, 1,
      "The 'details' array property should contains 1 element");
   ok(result.exceptionInfo.details[0].includes("cyclic object value"),
      "Got the expected content in the error results's details");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_system_principal_denied_error_inspectedWindowEval_result() {
-  const {client, inspectedWindowFront} = await setup("about:addons");
-  const result = await inspectedWindowFront.eval(FAKE_CALLER_INFO, "window", {});
+  const {
+    client, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
+  } = await setup("about:addons");
+
+  const result = await inspectedWindowFront.eval(fakeExtCallerInfo, "window", {});
 
   ok(!result.value, "Got a null result from inspectedWindow eval");
   ok(result.exceptionInfo.isError,
      "Got an API Error result from inspectedWindow eval on a system principal page");
   is(result.exceptionInfo.code, "E_PROTOCOLERROR",
      "Got the expected 'code' property in the error result");
   is(result.exceptionInfo.description, "Inspector protocol error: %s",
      "Got the expected 'description' property in the error result");
   is(result.exceptionInfo.details.length, 1,
      "The 'details' array property should contains 1 element");
   is(result.exceptionInfo.details[0],
      "This target has a system principal. inspectedWindow.eval denied.",
      "Got the expected content in the error results's details");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowEval_result() {
-  const {client, inspectedWindowFront} = await setup(MAIN_DOMAIN);
+  const {
+    client, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
+  } = await setup(MAIN_DOMAIN);
+
   const result = await inspectedWindowFront.eval(
-    FAKE_CALLER_INFO, "throw Error('fake eval error');", {});
+    fakeExtCallerInfo, "throw Error('fake eval error');", {});
 
   ok(result.exceptionInfo.isException, "Got an exception as expected");
   ok(!result.value, "Got an undefined eval value");
   ok(!result.exceptionInfo.isError, "An exception should not be isError=true");
   ok(result.exceptionInfo.value.includes("Error: fake eval error"),
      "Got the expected exception message");
 
   const expectedCallerInfo =
-    `called from ${FAKE_CALLER_INFO.url}:${FAKE_CALLER_INFO.lineNumber}`;
+    `called from ${fakeExtCallerInfo.url}:${fakeExtCallerInfo.lineNumber}`;
   ok(result.exceptionInfo.value.includes(expectedCallerInfo),
      "Got the expected caller info in the exception message");
 
   const expectedStack = `eval code:1:7`;
   ok(result.exceptionInfo.value.includes(expectedStack),
      "Got the expected stack trace in the exception message");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload() {
   const {
     client, consoleClient, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=cache`);
 
   // Test reload with bypassCache=false.
 
   const waitForNoBypassCacheReload = waitForNextTabNavigated(client);
-  const reloadResult = await inspectedWindowFront.reload(FAKE_CALLER_INFO,
+  const reloadResult = await inspectedWindowFront.reload(fakeExtCallerInfo,
                                                          {ignoreCache: false});
 
   ok(!reloadResult, "Got the expected undefined result from inspectedWindow reload");
 
   await waitForNoBypassCacheReload;
 
   const noBypassCacheEval = await consoleEvalJS(consoleClient,
                                                 "document.body.textContent");
 
   is(noBypassCacheEval.result, "empty cache headers",
      "Got the expected result with reload forceBypassCache=false");
 
   // Test reload with bypassCache=true.
 
   const waitForForceBypassCacheReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO, {ignoreCache: true});
+  await inspectedWindowFront.reload(fakeExtCallerInfo, {ignoreCache: true});
 
   await waitForForceBypassCacheReload;
 
   const forceBypassCacheEval = await consoleEvalJS(consoleClient,
                                                    "document.body.textContent");
 
   is(forceBypassCacheEval.result, "no-cache:no-cache",
      "Got the expected result with reload forceBypassCache=true");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_customUserAgent() {
   const {
     client, consoleClient, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=user-agent`);
 
   // Test reload with custom userAgent.
 
   const waitForCustomUserAgentReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO,
+  await inspectedWindowFront.reload(fakeExtCallerInfo,
                                     {userAgent: "Customized User Agent"});
 
   await waitForCustomUserAgentReload;
 
   const customUserAgentEval = await consoleEvalJS(consoleClient,
                                                   "document.body.textContent");
 
   is(customUserAgentEval.result, "Customized User Agent",
      "Got the expected result on reload with a customized userAgent");
 
   // Test reload with no custom userAgent.
 
   const waitForNoCustomUserAgentReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO, {});
+  await inspectedWindowFront.reload(fakeExtCallerInfo, {});
 
   await waitForNoCustomUserAgentReload;
 
   const noCustomUserAgentEval = await consoleEvalJS(consoleClient,
                                                     "document.body.textContent");
 
   is(noCustomUserAgentEval.result, window.navigator.userAgent,
      "Got the expected result with reload without a customized userAgent");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_injectedScript() {
   const {
     client, consoleClient, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=injected-script&frames=3`);
 
   // Test reload with an injectedScript.
 
   const waitForInjectedScriptReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO,
+  await inspectedWindowFront.reload(fakeExtCallerInfo,
                                     {injectedScript: `new ${injectedScript}`});
   await waitForInjectedScriptReload;
 
   const injectedScriptEval = await consoleEvalJS(consoleClient,
                                                  `(${collectEvalResults})()`);
 
-  const expectedResult = (new Array(4)).fill("injected script executed first");
+  const expectedResult = (new Array(5)).fill("injected script executed first");
 
   SimpleTest.isDeeply(JSON.parse(injectedScriptEval.result), expectedResult,
      "Got the expected result on reload with an injected script");
 
   // Test reload without an injectedScript.
 
   const waitForNoInjectedScriptReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO, {});
+  await inspectedWindowFront.reload(fakeExtCallerInfo, {});
   await waitForNoInjectedScriptReload;
 
   const noInjectedScriptEval = await consoleEvalJS(consoleClient,
                                                    `(${collectEvalResults})()`);
 
-  const newExpectedResult = (new Array(4)).fill("injected script NOT executed");
+  const newExpectedResult = (new Array(5)).fill("injected script NOT executed");
 
   SimpleTest.isDeeply(JSON.parse(noInjectedScriptEval.result), newExpectedResult,
                       "Got the expected result on reload with no injected script");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_multiple_calls() {
   const {
     client, consoleClient, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=user-agent`);
 
   // Test reload with custom userAgent three times (and then
   // check that only the first one has affected the page reload.
 
   const waitForCustomUserAgentReload = waitForNextTabNavigated(client);
 
-  inspectedWindowFront.reload(FAKE_CALLER_INFO, {userAgent: "Customized User Agent 1"});
-  inspectedWindowFront.reload(FAKE_CALLER_INFO, {userAgent: "Customized User Agent 2"});
+  inspectedWindowFront.reload(fakeExtCallerInfo, {userAgent: "Customized User Agent 1"});
+  inspectedWindowFront.reload(fakeExtCallerInfo, {userAgent: "Customized User Agent 2"});
 
   await waitForCustomUserAgentReload;
 
   const customUserAgentEval = await consoleEvalJS(consoleClient,
                                                   "document.body.textContent");
 
   is(customUserAgentEval.result, "Customized User Agent 1",
      "Got the expected result on reload with a customized userAgent");
 
   // Test reload with no custom userAgent.
 
   const waitForNoCustomUserAgentReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO, {});
+  await inspectedWindowFront.reload(fakeExtCallerInfo, {});
 
   await waitForNoCustomUserAgentReload;
 
   const noCustomUserAgentEval = await consoleEvalJS(consoleClient,
                                                     "document.body.textContent");
 
   is(noCustomUserAgentEval.result, window.navigator.userAgent,
      "Got the expected result with reload without a customized userAgent");
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 add_task(async function test_exception_inspectedWindowReload_stopped() {
   const {
     client, consoleClient, inspectedWindowFront,
+    extension, fakeExtCallerInfo,
   } = await setup(`${TEST_RELOAD_URL}?test=injected-script&frames=3`);
 
   // Test reload on a page that calls window.stop() immediately during the page loading
 
   const waitForPageLoad = waitForNextTabNavigated(client);
-  await inspectedWindowFront.eval(FAKE_CALLER_INFO,
+  await inspectedWindowFront.eval(fakeExtCallerInfo,
                                   "window.location += '&stop=windowStop'");
 
   info("Load a webpage that calls 'window.stop()' while is still loading");
   await waitForPageLoad;
 
   info("Starting a reload with an injectedScript");
   const waitForInjectedScriptReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO,
+  await inspectedWindowFront.reload(fakeExtCallerInfo,
                                     {injectedScript: `new ${injectedScript}`});
   await waitForInjectedScriptReload;
 
   const injectedScriptEval = await consoleEvalJS(consoleClient,
                                                  `(${collectEvalResults})()`);
 
   // The page should have stopped during the reload and only one injected script
   // is expected.
@@ -373,28 +415,28 @@ add_task(async function test_exception_i
 
   SimpleTest.isDeeply(JSON.parse(injectedScriptEval.result), expectedResult,
      "The injected script has been executed on the 'stopped' page reload");
 
   // Reload again with no options.
 
   info("Reload the tab again without any reload options");
   const waitForNoInjectedScriptReload = waitForNextTabNavigated(client);
-  await inspectedWindowFront.reload(FAKE_CALLER_INFO, {});
+  await inspectedWindowFront.reload(fakeExtCallerInfo, {});
   await waitForNoInjectedScriptReload;
 
   const noInjectedScriptEval = await consoleEvalJS(consoleClient,
                                                    `(${collectEvalResults})()`);
 
   // The page should have stopped during the reload and no injected script should
   // have been executed during this second reload (or it would mean that the previous
   // customized reload was still pending and has wrongly affected the second reload)
   const newExpectedResult = (new Array(1)).fill("injected script NOT executed");
 
   SimpleTest.isDeeply(
     JSON.parse(noInjectedScriptEval.result), newExpectedResult,
     "No injectedScript should have been evaluated during the second reload"
   );
 
-  await teardown({client});
+  await teardown({client, extension});
 });
 
 // TODO: check eval with $0 binding once implemented (Bug 1300590)
--- a/devtools/server/tests/browser/inspectedwindow-reload-target.sjs
+++ b/devtools/server/tests/browser/inspectedwindow-reload-target.sjs
@@ -43,16 +43,24 @@ function handleInjectedScriptTestRequest
 
   const frames = parseInt(params.get("frames"));
   let content = "";
 
   if (frames > 0) {
     // Output an iframe in seamless mode, so that there is an higher chance that in case
     // of test failures we get a screenshot where the nested iframes are all visible.
     content = `<iframe seamless src="?test=injected-script&frames=${frames - 1}"></iframe>`;
+  } else {
+    // Output an about:srcdoc frame to be sure that inspectedWindow.eval is able to
+    // evaluate js code into it.
+    let srcdoc = `
+      <pre>injected script NOT executed</pre>
+      <script>window.pageScriptExecutedFirst = true</script>
+    `;
+    content = `<iframe style="height: 30px;" srcdoc="${srcdoc}"></iframe>`;
   }
 
   if (params.get("stop") == "windowStop") {
     content = "<script>window.stop();</script>" + content;
   }
 
   response.write(`<!DOCTYPE html>
     <html>
--- a/dom/chrome-webidl/WebExtensionPolicy.webidl
+++ b/dom/chrome-webidl/WebExtensionPolicy.webidl
@@ -153,16 +153,21 @@ interface WebExtensionPolicy {
   static WebExtensionPolicy? getByHostname(ByteString hostname);
 
   /**
    * Returns the currently-active policy for the extension extension URI, or
    * null if the URI is not an extension URI, or no policy is currently active
    * for it.
    */
   static WebExtensionPolicy? getByURI(URI uri);
+
+  /**
+   * Returns true if the URI is restricted for any extension.
+   */
+  static boolean isRestrictedURI(URI uri);
 };
 
 dictionary WebExtensionInit {
   required DOMString id;
 
   required ByteString mozExtensionHostname;
 
   required DOMString baseURL;
--- a/toolkit/components/extensions/WebExtensionPolicy.h
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -144,16 +144,22 @@ public:
   GetByID(dom::GlobalObject& aGlobal, const nsAString& aID);
 
   static already_AddRefed<WebExtensionPolicy>
   GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname);
 
   static already_AddRefed<WebExtensionPolicy>
   GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI);
 
+  static bool
+  IsRestrictedURI(dom::GlobalObject& aGlobal, const URLInfo& aURI)
+  {
+    return IsRestrictedURI(aURI);
+  }
+
 
   static bool UseRemoteWebExtensions(dom::GlobalObject& aGlobal);
   static bool IsExtensionProcess(dom::GlobalObject& aGlobal);
 
 
   nsISupports* GetParentObject() const { return mParent; }
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override;