[Extensions] Restrict debugging file:-scheme URLs

Don't allow extensions to debug file:-scheme URLs if the extension does
not have explicit file access (as set in chrome://extensions). Achieve
this by introducing a new virtual method on DevToolsAgentHostClient to
allow the implementor to check if a given host is allowed to be
inspected.

Add regression tests for the same.

Bug: 666299

Change-Id: Icb5ee89bf788643eee166eef83802d10ab825a6c
Reviewed-on: https://chromium-review.googlesource.com/1104954
Commit-Queue: Devlin <[email protected]>
Reviewed-by: Dmitry Gozman <[email protected]>
Cr-Commit-Position: refs/heads/master@{#569828}
diff --git a/chrome/browser/extensions/api/debugger/debugger_api.cc b/chrome/browser/extensions/api/debugger/debugger_api.cc
index 94097ccd7..c3ce2dd 100644
--- a/chrome/browser/extensions/api/debugger/debugger_api.cc
+++ b/chrome/browser/extensions/api/debugger/debugger_api.cc
@@ -29,6 +29,7 @@
 #include "chrome/browser/extensions/api/debugger/extension_dev_tools_infobar.h"
 #include "chrome/browser/extensions/extension_service.h"
 #include "chrome/browser/extensions/extension_tab_util.h"
+#include "chrome/browser/extensions/extension_util.h"
 #include "chrome/browser/infobars/infobar_service.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/browser.h"
@@ -83,6 +84,26 @@
     dst->target_id.reset(new std::string(*src.target_id));
 }
 
+// Returns true if the given |Extension| is allowed to attach to the specified
+// |url|.
+bool ExtensionCanAttachToURL(const Extension& extension,
+                             const GURL& url,
+                             Profile* profile,
+                             std::string* error) {
+  // NOTE: The `debugger` permission implies all URLs access (and indicates
+  // such to the user), so we don't check explicit page access. However, we
+  // still need to check if it's an otherwise-restricted URL.
+  if (extension.permissions_data()->IsRestrictedUrl(url, error))
+    return false;
+
+  if (url.SchemeIsFile() && !util::AllowFileAccess(extension.id(), profile)) {
+    *error = debugger_api_constants::kRestrictedError;
+    return false;
+  }
+
+  return true;
+}
+
 }  // namespace
 
 // ExtensionDevToolsClientHost ------------------------------------------------
@@ -97,14 +118,13 @@
  public:
   ExtensionDevToolsClientHost(Profile* profile,
                               DevToolsAgentHost* agent_host,
-                              const std::string& extension_id,
-                              const std::string& extension_name,
+                              scoped_refptr<const Extension> extension,
                               const Debuggee& debuggee);
 
   ~ExtensionDevToolsClientHost() override;
 
   bool Attach();
-  const std::string& extension_id() { return extension_id_; }
+  const std::string& extension_id() { return extension_->id(); }
   DevToolsAgentHost* agent_host() { return agent_host_.get(); }
   void RespondDetachedToPendingRequests();
   void Close();
@@ -119,6 +139,8 @@
   void AgentHostClosed(DevToolsAgentHost* agent_host) override;
   void DispatchProtocolMessage(DevToolsAgentHost* agent_host,
                                const std::string& message) override;
+  bool MayAttachToRenderer(content::RenderFrameHost* render_frame_host,
+                           bool is_webui) override;
 
  private:
   using PendingRequests =
@@ -138,8 +160,7 @@
 
   Profile* profile_;
   scoped_refptr<DevToolsAgentHost> agent_host_;
-  std::string extension_id_;
-  std::string extension_name_;
+  scoped_refptr<const Extension> extension_;
   Debuggee debuggee_;
   content::NotificationRegistrar registrar_;
   int last_request_id_;
@@ -157,13 +178,11 @@
 ExtensionDevToolsClientHost::ExtensionDevToolsClientHost(
     Profile* profile,
     DevToolsAgentHost* agent_host,
-    const std::string& extension_id,
-    const std::string& extension_name,
+    scoped_refptr<const Extension> extension,
     const Debuggee& debuggee)
     : profile_(profile),
       agent_host_(agent_host),
-      extension_id_(extension_id),
-      extension_name_(extension_name),
+      extension_(std::move(extension)),
       last_request_id_(0),
       infobar_(nullptr),
       detach_reason_(api::debugger::DETACH_REASON_TARGET_CLOSED),
@@ -195,17 +214,11 @@
 
   // We allow policy-installed extensions to circumvent the normal
   // infobar warning. See crbug.com/693621.
-  const Extension* extension =
-      ExtensionRegistry::Get(profile_)->enabled_extensions().GetByID(
-          extension_id_);
-  // TODO(dgozman): null-checking |extension| below is sketchy.
-  // We probably should not allow debugging in this case. Or maybe
-  // it's never null?
-  if (extension && Manifest::IsPolicyLocation(extension->location()))
+  if (Manifest::IsPolicyLocation(extension_->location()))
     return true;
 
   infobar_ = ExtensionDevToolsInfoBar::Create(
-      extension_id_, extension_name_, this,
+      extension_id(), extension_->name(), this,
       base::Bind(&ExtensionDevToolsClientHost::InfoBarDismissed,
                  base::Unretained(this)));
   return true;
@@ -272,15 +285,15 @@
   auto event =
       std::make_unique<Event>(events::DEBUGGER_ON_DETACH, OnDetach::kEventName,
                               std::move(args), profile_);
-  EventRouter::Get(profile_)
-      ->DispatchEventToExtension(extension_id_, std::move(event));
+  EventRouter::Get(profile_)->DispatchEventToExtension(extension_id(),
+                                                       std::move(event));
 }
 
 void ExtensionDevToolsClientHost::OnExtensionUnloaded(
     content::BrowserContext* browser_context,
     const Extension* extension,
     UnloadedExtensionReason reason) {
-  if (extension->id() == extension_id_)
+  if (extension->id() == extension_id())
     Close();
 }
 
@@ -320,8 +333,8 @@
     auto event =
         std::make_unique<Event>(events::DEBUGGER_ON_EVENT, OnEvent::kEventName,
                                 std::move(args), profile_);
-    EventRouter::Get(profile_)
-        ->DispatchEventToExtension(extension_id_, std::move(event));
+    EventRouter::Get(profile_)->DispatchEventToExtension(extension_id(),
+                                                         std::move(event));
   } else {
     DebuggerSendCommandFunction* function = pending_requests_[id].get();
     if (!function)
@@ -332,6 +345,31 @@
   }
 }
 
+bool ExtensionDevToolsClientHost::MayAttachToRenderer(
+    content::RenderFrameHost* render_frame_host,
+    bool is_webui) {
+  if (is_webui)
+    return false;
+
+  if (!render_frame_host)
+    return true;
+
+  std::string error;
+  // We check the site instance URL here (instead of
+  // RenderFrameHost::GetLastCommittedURL()) because it's too early in the
+  // navigation for anything else.
+  const GURL& site_instance_url =
+      render_frame_host->GetSiteInstance()->GetSiteURL();
+
+  if (site_instance_url.is_empty()) {
+    // |site_instance_url| is empty for about:blank. Allow the extension to
+    // attach.
+    return true;
+  }
+
+  return ExtensionCanAttachToURL(*extension_, site_instance_url, profile_,
+                                 &error);
+}
 
 // DebuggerFunction -----------------------------------------------------------
 
@@ -366,8 +404,10 @@
     if (result && web_contents) {
       // TODO(rdevlin.cronin) This should definitely be GetLastCommittedURL().
       GURL url = web_contents->GetVisibleURL();
-      if (extension()->permissions_data()->IsRestrictedUrl(url, &error_))
+
+      if (!ExtensionCanAttachToURL(*extension(), url, GetProfile(), &error_))
         return false;
+
       agent_host_ = DevToolsAgentHost::GetOrCreateFor(web_contents);
     }
   } else if (debuggee_.extension_id) {
@@ -463,8 +503,7 @@
   }
 
   auto host = std::make_unique<ExtensionDevToolsClientHost>(
-      GetProfile(), agent_host_.get(), extension()->id(), extension()->name(),
-      debuggee_);
+      GetProfile(), agent_host_.get(), extension(), debuggee_);
 
   if (!host->Attach()) {
     FormatErrorMessage(debugger_api_constants::kRestrictedError);