Anchor target=_blank implies rel=noopener

To mitigate "tab-napping" attacks, in which a new tab/window opened by
a victim context may navigate that opener context, the HTML standard
changed to specify that anchors that target _blank should behave as if
|rel="noopener"| is set. A page wishing to opt out of this behavior may
set |rel="opener"|.

Bug: 898942
Change-Id: Id34bbc480e96cb2cc8e922388a9a5bc4161b03b1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1630010
Reviewed-by: Julian Pastarmov <[email protected]>
Reviewed-by: John Abd-El-Malek <[email protected]>
Reviewed-by: Marijn Kruisselbrink <[email protected]>
Reviewed-by: Mike West <[email protected]>
Reviewed-by: Arthur Sonzogni <[email protected]>
Commit-Queue: Eric Lawrence [MSFT] <[email protected]>
Cr-Commit-Position: refs/heads/master@{#825022}
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index f140422..c7e9d29 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -2326,6 +2326,12 @@
             blink::switches::kUserAgentClientHintDisable);
       }
 
+      if (!local_state->GetBoolean(
+              policy::policy_prefs::kTargetBlankImpliesNoOpener)) {
+        command_line->AppendSwitch(
+            switches::kDisableTargetBlankImpliesNoOpener);
+      }
+
 #if defined(OS_ANDROID)
       // Communicating to content/ for BackForwardCache.
       if (prefs->HasPrefPath(policy::policy_prefs::kBackForwardCacheEnabled) &&
diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
index b4bd7c6..90debc9 100644
--- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc
+++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
@@ -1300,6 +1300,9 @@
   { key::kLookalikeWarningAllowlistDomains,
     prefs::kLookalikeWarningAllowlistDomains,
     base::Value::Type::LIST },
+  { key::kTargetBlankImpliesNoOpener,
+    policy::policy_prefs::kTargetBlankImpliesNoOpener,
+    base::Value::Type::BOOLEAN },
 
 #if defined(OS_ANDROID)
   { key::kTosDialogBehavior,
diff --git a/chrome/browser/policy/window_opener_policy_browsertest.cc b/chrome/browser/policy/window_opener_policy_browsertest.cc
new file mode 100644
index 0000000..3cbf1ac
--- /dev/null
+++ b/chrome/browser/policy/window_opener_policy_browsertest.cc
@@ -0,0 +1,71 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// TODO(crbug.com/898942): Remove this in Chrome 95.
+#include "base/values.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/policy/policy_test_utils.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "components/policy/core/common/policy_map.h"
+#include "components/policy/core/common/policy_pref_names.h"
+#include "components/policy/core/common/policy_types.h"
+#include "components/policy/policy_constants.h"
+#include "components/prefs/pref_service.h"
+#include "content/public/browser/web_contents.h"
+#include "content/public/test/browser_test.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace policy {
+
+class PolicyTestWindowOpener : public PolicyTest {
+  void SetUpInProcessBrowserTestFixture() override {
+    PolicyTest::SetUpInProcessBrowserTestFixture();
+    PolicyMap policies;
+    // Configure the policy to disable the new behavior.
+    SetPolicy(&policies, policy::key::kTargetBlankImpliesNoOpener,
+              base::Value(false));
+    provider_.UpdateChromePolicy(policies);
+  }
+};
+
+// Check that when the TargetBlankImpliesNoOpener policy is configured and set
+// to false, windows targeting _blank do not have their opener cleared.
+IN_PROC_BROWSER_TEST_F(PolicyTestWindowOpener, CheckWindowOpenerNonNull) {
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  PrefService* local_state = g_browser_process->local_state();
+  EXPECT_FALSE(local_state->GetBoolean(
+      policy::policy_prefs::kTargetBlankImpliesNoOpener));
+
+  GURL url(
+      "data:text/html,<a href='about:blank' target='_blank' "
+      "id='link'>popup</a>");
+  ui_test_utils::NavigateToURL(browser(), url);
+
+  ASSERT_EQ(browser()->tab_strip_model()->count(), 1);
+  content::WebContents* tab_1 =
+      browser()->tab_strip_model()->GetActiveWebContents();
+
+  ui_test_utils::TabAddedWaiter tab_Added_waiter(browser());
+  SimulateMouseClickOrTapElementWithId(tab_1, "link");
+  tab_Added_waiter.Wait();
+  content::WebContents* tab_2 =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_NE(tab_1, tab_2);
+
+  constexpr char kScript[] =
+      R"({ window.domAutomationController.send(window.opener === null); })";
+  content::ExecuteScriptAsync(tab_2, kScript);
+
+  content::DOMMessageQueue message_queue;
+  std::string message;
+  EXPECT_TRUE(message_queue.WaitForMessage(&message));
+  EXPECT_EQ("false", message);
+}
+
+}  // namespace policy
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index 43fe63e..b1817554 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -604,6 +604,8 @@
       policy::policy_prefs::kIntensiveWakeUpThrottlingEnabled, false);
   registry->RegisterBooleanPref(
       policy::policy_prefs::kUserAgentClientHintsEnabled, true);
+  registry->RegisterBooleanPref(
+      policy::policy_prefs::kTargetBlankImpliesNoOpener, true);
 #if defined(OS_ANDROID)
   registry->RegisterBooleanPref(policy::policy_prefs::kBackForwardCacheEnabled,
                                 true);
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index afaefac..5c48ad6 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -1238,6 +1238,7 @@
       "../browser/policy/signed_exchange_browsertest.cc",
       "../browser/policy/site_isolation_policy_browsertest.cc",
       "../browser/policy/url_blacklist_policy_browsertest.cc",
+      "../browser/policy/window_opener_policy_browsertest.cc",
       "../browser/portal/portal_browsertest.cc",
       "../browser/portal/portal_recently_audible_browsertest.cc",
       "../browser/predictors/loading_predictor_browsertest.cc",
diff --git a/chrome/test/data/chromedriver/page_test.html b/chrome/test/data/chromedriver/page_test.html
index e0f7a43d..ebfa0620 100644
--- a/chrome/test/data/chromedriver/page_test.html
+++ b/chrome/test/data/chromedriver/page_test.html
@@ -4,6 +4,6 @@
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 </head>
 <body>
-<a id="link" target="_blank" href="empty.html">Link to empty.html</a>
+<a id="link" rel="opener" target="_blank" href="empty.html">Link to empty.html</a>
 </body>
 </html>
diff --git a/chrome/test/data/extensions/api_test/protocol_handler/test_registration.js b/chrome/test/data/extensions/api_test/protocol_handler/test_registration.js
index 039137b..6b3d86c 100644
--- a/chrome/test/data/extensions/api_test/protocol_handler/test_registration.js
+++ b/chrome/test/data/extensions/api_test/protocol_handler/test_registration.js
@@ -26,6 +26,7 @@
   const url = `${scheme}:path`;
   const a = document.body.appendChild(document.createElement('a'));
   a.href = url;
+  a.rel = 'opener';
   a.target = '_blank';
   return new Promise((resolve, reject) => {
     window.addEventListener('message', function(event) {
diff --git a/chrome/test/data/extensions/api_test/webnavigation/targetBlank/a.html b/chrome/test/data/extensions/api_test/webnavigation/targetBlank/a.html
index 5c9cff1..7e1d526 100644
--- a/chrome/test/data/extensions/api_test/webnavigation/targetBlank/a.html
+++ b/chrome/test/data/extensions/api_test/webnavigation/targetBlank/a.html
@@ -6,6 +6,6 @@
 </style>
 </head>
 <body>
-<a target="_blank" href="b.html">link</a>
+<a target="_blank" rel="opener" href="b.html">link</a>
 </body>
 </html>
diff --git a/chrome/test/data/policy/policy_test_cases.json b/chrome/test/data/policy/policy_test_cases.json
index 999b798..d45afbf 100644
--- a/chrome/test/data/policy/policy_test_cases.json
+++ b/chrome/test/data/policy/policy_test_cases.json
@@ -3726,6 +3726,21 @@
     "policy_pref_mapping_tests": { "scroll_to_text_fragment_enabled": {} }
   },
 
+  "TargetBlankImpliesNoOpener": {
+    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "policy_pref_mapping_test": [
+      {
+        "policies": { "TargetBlankImpliesNoOpener": false },
+        "prefs": {
+          "policy.target_blank_implies_noopener": {
+            "local_state": true,
+            "value": false
+          }
+        }
+      }
+    ]
+  },
+
   "GloballyScopeHTTPAuthCacheEnabled": {
     "os": ["win", "linux", "mac", "chromeos", "android"],
     "test_policy": { "GloballyScopeHTTPAuthCacheEnabled": true },
diff --git a/chrome/test/data/popup_blocker/popup-in-href.html b/chrome/test/data/popup_blocker/popup-in-href.html
index 39223ab3..9fd8bbf5 100644
--- a/chrome/test/data/popup_blocker/popup-in-href.html
+++ b/chrome/test/data/popup_blocker/popup-in-href.html
@@ -1,6 +1,7 @@
 <!doctype html>
 <html>
 <body>
-<a id="link" href="javascript:window.open('https://example.com');" target="_blank">open</a>.
+<a id="link" rel="opener" href="javascript:window.open('https://example.com');"
+ target="_blank">open</a>.
 </body>
 </html>
diff --git a/chrome/test/data/protocol_handler/service_workers/test_protocol_handler_and_service_workers.js b/chrome/test/data/protocol_handler/service_workers/test_protocol_handler_and_service_workers.js
index dc91a4c..a47b4290 100644
--- a/chrome/test/data/protocol_handler/service_workers/test_protocol_handler_and_service_workers.js
+++ b/chrome/test/data/protocol_handler/service_workers/test_protocol_handler_and_service_workers.js
@@ -24,6 +24,7 @@
 async function handledByServiceWorker(url) {
   const a = document.body.appendChild(document.createElement('a'));
   a.href = url;
+  a.rel = 'opener';
   a.target = '_blank';
   let handled_by_service_worker;
   await new Promise(resolve => {