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 <pastarmovj@chromium.org>
Reviewed-by: John Abd-El-Malek <jam@chromium.org>
Reviewed-by: Marijn Kruisselbrink <mek@chromium.org>
Reviewed-by: Mike West <mkwst@chromium.org>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Commit-Queue: Eric Lawrence [MSFT] <ericlaw@microsoft.com>
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..b181755 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..9fd8bbf 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 => {
diff --git a/components/policy/core/common/policy_pref_names.cc b/components/policy/core/common/policy_pref_names.cc
index ec602d3..120d915 100644
--- a/components/policy/core/common/policy_pref_names.cc
+++ b/components/policy/core/common/policy_pref_names.cc
@@ -56,6 +56,11 @@
 const char kUserAgentClientHintsEnabled[] =
     "policy.user_agent_client_hints_enabled";
 
+// Boolean that controls whether a window spawned from an anchor targeting
+// _blank receives an opener. TODO(crbug.com/898942): Remove this in Chrome 95.
+const char kTargetBlankImpliesNoOpener[] =
+    "policy.target_blank_implies_noopener";
+
 #if defined(OS_ANDROID)
 // Boolean policy preference to disable the BackForwardCache feature.
 const char kBackForwardCacheEnabled[] = "policy.back_forward_cache_enabled";
diff --git a/components/policy/core/common/policy_pref_names.h b/components/policy/core/common/policy_pref_names.h
index 48b0691..e2ef630 100644
--- a/components/policy/core/common/policy_pref_names.h
+++ b/components/policy/core/common/policy_pref_names.h
@@ -23,6 +23,7 @@
 POLICY_EXPORT extern const char kUserPolicyRefreshRate[];
 POLICY_EXPORT extern const char kIntensiveWakeUpThrottlingEnabled[];
 POLICY_EXPORT extern const char kUserAgentClientHintsEnabled[];
+POLICY_EXPORT extern const char kTargetBlankImpliesNoOpener[];
 #if defined(OS_ANDROID)
 POLICY_EXPORT extern const char kBackForwardCacheEnabled[];
 #endif  // defined(OS_ANDROID)
diff --git a/components/policy/resources/policy_templates.json b/components/policy/resources/policy_templates.json
index 337cfc1..fd86418 100644
--- a/components/policy/resources/policy_templates.json
+++ b/components/policy/resources/policy_templates.json
@@ -7455,6 +7455,29 @@
       'desc': '''This policy only takes effect when the policy <ph name="SECURITY_TOKEN_SESSION_BEHAVIOR_POLICY_NAME">SecurityTokenSessionBehavior</ph> is set to <ph name="SECURITY_TOKEN_SESSION_BEHAVIOR_LOCK">LOCK</ph> or <ph name="SECURITY_TOKEN_SESSION_BEHAVIOR_LOGOUT">LOGOUT</ph>, and a user who authenticates via a smart card removes that smart card. Then, this policy specifies for how many seconds a notification which informs the user of the impending action is displayed. This notification is blocking the screen. The action will only happen after this notification expires. The user can prevent the action from happening by re-inserting the smart card before the notification expires. If this policy is set to zero, no notification will be displayed and the action happens immediately.'''
     },
     {
+      'name': 'TargetBlankImpliesNoOpener',
+      'owners': ['ericlaw@microsoft.com'],
+      'type': 'main',
+      'schema': { 'type': 'boolean' },
+      'supported_on': ['chrome.*:88-', 'chrome_os:88-', 'android:88-'],
+      'features': {
+        'dynamic_refresh': False,
+        'per_profile': False,
+      },
+      'example_value': False,
+      'default': True,
+      'id': 802,
+      'caption': '''Do not set <ph name="WINDOW_OPENER_PROPERTY">window.opener</ph> for links targeting <ph name="BLANK_PAGE_NAME">_blank</ph>''',
+      'tags': [],
+      'desc': '''Setting the policy to Disabled allows popups targeting <ph name="BLANK_PAGE_NAME">_blank</ph> to access (via JavaScript) the page that requested to open the popup.
+
+      Setting the policy to Enabled or leaving it unset causes the <ph name="WINDOW_OPENER_PROPERTY">window.opener</ph> property to be set to <ph name="NULL_VALUE">null</ph> unless the anchor specifies <ph name="REL_OPENER_ATTRIBUTE">rel="opener"</ph>.
+
+      This policy will be removed in <ph name="PRODUCT_NAME">$1<ex>Google Chrome</ex></ph> version 95.
+
+      See https://chromestatus.com/feature/6140064063029248.''',
+    },
+    {
       'name': 'InstantEnabled',
       'owners': ['file://components/policy/resources/OWNERS'],
       'type': 'main',
@@ -24248,6 +24271,6 @@
   'placeholders': [],
   'deleted_policy_ids': [114, 115, 204, 205, 206, 412, 476, 544, 546, 562, 569, 578, 583, 585, 586, 587, 588, 589, 590, 591, 600, 668, 669],
   'deleted_atomic_policy_group_ids': [19],
-  'highest_id_currently_used': 801,
+  'highest_id_currently_used': 802,
   'highest_atomic_group_id_currently_used': 40
 }
diff --git a/content/browser/blob_storage/blob_url_browsertest.cc b/content/browser/blob_storage/blob_url_browsertest.cc
index 5c7d96e..0c85116 100644
--- a/content/browser/blob_storage/blob_url_browsertest.cc
+++ b/content/browser/blob_storage/blob_url_browsertest.cc
@@ -117,7 +117,7 @@
       "link.innerText = 'Click Me!';"
       "link.href = 'blob:http://spoof.com@' + "
       "    URL.createObjectURL(new Blob(['potato'])).split('://')[1];"
-      "link.target = '_blank';"
+      "link.rel = 'opener'; link.target = '_blank';"
       "link.click()"));
 
   // The link should create a new tab.
diff --git a/content/browser/renderer_host/render_frame_host_manager_browsertest.cc b/content/browser/renderer_host/render_frame_host_manager_browsertest.cc
index 63c8004..1a8aac99 100644
--- a/content/browser/renderer_host/render_frame_host_manager_browsertest.cc
+++ b/content/browser/renderer_host/render_frame_host_manager_browsertest.cc
@@ -93,6 +93,7 @@
     "(function(url) {\n"
     "  var lnk = document.createElement(\"a\");\n"
     "  lnk.href = url;\n"
+    "  lnk.rel = 'opener';\n"
     "  lnk.target = \"_blank\";\n"
     "  document.body.appendChild(lnk);\n"
     "  lnk.click();\n"
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index f19c4a8..49bc20d 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -368,6 +368,8 @@
            blink::features::kParentNodeReplaceChildren},
           {"RawClipboard", blink::features::kRawClipboard},
           {"StorageAccessAPI", blink::features::kStorageAccessAPI},
+          {"TargetBlankImpliesNoOpener",
+           blink::features::kTargetBlankImpliesNoOpener},
           {"TrustedDOMTypes", features::kTrustedDOMTypes},
           {"UserAgentClientHint", features::kUserAgentClientHint},
           {"WebAppManifestDisplayOverride",
@@ -410,6 +412,8 @@
       {wrf::EnablePresentationAPI, switches::kDisablePresentationAPI, false},
       {wrf::EnableRemotePlaybackAPI, switches::kDisableRemotePlaybackAPI,
        false},
+      {wrf::EnableTargetBlankImpliesNoOpener,
+       switches::kDisableTargetBlankImpliesNoOpener, false},
       {wrf::EnableTimerThrottlingForBackgroundTabs,
        switches::kDisableBackgroundTimerThrottling, false},
       // End of Stable Features
diff --git a/content/public/common/content_switches.cc b/content/public/common/content_switches.cc
index 33b83266..f477b22 100644
--- a/content/public/common/content_switches.cc
+++ b/content/public/common/content_switches.cc
@@ -258,6 +258,11 @@
 // Disables the speech synthesis part of Web Speech API.
 const char kDisableSpeechSynthesisAPI[]     = "disable-speech-synthesis-api";
 
+// Used to communicate managed policy for the TargetBlankImpliesNoOpenerDisable
+// behavioral change.
+extern const char kDisableTargetBlankImpliesNoOpener[] =
+    "target-blank-implies-no-opener-disable";
+
 // Disables adding the test certs in the network process.
 const char kDisableTestCerts[]              = "disable-test-root-certs";
 
diff --git a/content/public/common/content_switches.h b/content/public/common/content_switches.h
index 4ffeefb..8574587 100644
--- a/content/public/common/content_switches.h
+++ b/content/public/common/content_switches.h
@@ -87,6 +87,7 @@
 CONTENT_EXPORT extern const char kDisableSoftwareRasterizer[];
 CONTENT_EXPORT extern const char kDisableSpeechAPI[];
 CONTENT_EXPORT extern const char kDisableSpeechSynthesisAPI[];
+CONTENT_EXPORT extern const char kDisableTargetBlankImpliesNoOpener[];
 CONTENT_EXPORT extern const char kDisableTestCerts[];
 CONTENT_EXPORT extern const char kDisableThreadedCompositing[];
 extern const char kDisableV8IdleTasks[];
diff --git a/content/test/data/click-noreferrer-links.html b/content/test/data/click-noreferrer-links.html
index af4880f..31e7ad0 100644
--- a/content/test/data/click-noreferrer-links.html
+++ b/content/test/data/click-noreferrer-links.html
@@ -135,8 +135,8 @@
   same-site rel=noopener and target=foo</a><br>
 <a href="navigate_opener.html" id="samesite_targeted_link" target="foo">
   same-site target=foo</a><br>
-<a href="title2.html" id="samesite_tblank_link" target="_blank">
-  same-site target=_blank</a><br>
+<a href="title2.html" id="samesite_tblank_link" rel="opener" target="_blank">
+  same-site rel=opener target=_blank</a><br>
 <a href="about:blank" id="blank_targeted_link" target="foo">
   blank_targeted_link=foo</a><br>
 
@@ -151,7 +151,7 @@
    id="noopener_and_tblank_link" rel="noopener" target="_blank">
   rel=noopener and target=_blank</a><br>
 <a href="http://REPLACE/title2.html" id="tblank_link"
-   target="_blank">target=_blank</a><br>
+   rel="opener" target="_blank">rel=opener target=_blank</a><br>
 <a href="http://REPLACE/title2.html" id="noref_link"
    rel="noreferrer">rel=noreferrer</a><br>
 <a href="http://REPLACE/title2.html" id="noopener_link"
diff --git a/content/test/data/conversions/register_impression.js b/content/test/data/conversions/register_impression.js
index c7d7d21..70eba0f 100644
--- a/content/test/data/conversions/register_impression.js
+++ b/content/test/data/conversions/register_impression.js
@@ -46,6 +46,9 @@
 function createImpressionTagWithTarget(id, url, data, destination, target) {
   let anchor = document.createElement("a");
   anchor.href = url;
+  if (target === "_blank") {
+    anchor.rel = "opener";
+  }
   anchor.setAttribute("impressiondata", data);
   anchor.setAttribute("conversiondestination", destination);
   anchor.setAttribute("target", target);
diff --git a/content/test/data/simple_links.html b/content/test/data/simple_links.html
index 7703605..4cbfd6f9 100644
--- a/content/test/data/simple_links.html
+++ b/content/test/data/simple_links.html
@@ -53,9 +53,9 @@
 <a href="title2.html" id="same_site_link">same-site</a><br>
 <a href="http://foo.com/title2.html" id="cross_site_link">cross-site</a><br>
 <a href="view-source:about:blank" id="view_source_link">view-source:</a><br>
-<a href="title2.html" id="same_site_new_window_link" target="_blank">same-site new window</a>
-<a href="http://foo.com/title2.html" id="cross_site_new_window_link" target="_blank">cross-site new window</a>
-<a href="" id="linkToSelf" target="_blank">self new window</a>
+<a href="title2.html" id="same_site_new_window_link" rel="opener" target="_blank">same-site new window</a>
+<a href="http://foo.com/title2.html" id="cross_site_new_window_link" rel="opener" target="_blank">cross-site new window</a>
+<a href="" id="linkToSelf" rel="opener" target="_blank">self new window</a>
 <script>
   document.getElementById("linkToSelf").href = window.location.toString();
 </script>
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index eb5e543..aedccf7 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -813,5 +813,10 @@
 // with @keyframes rules in their stylesheets.
 const base::Feature kCSSKeyframesMemoryReduction{
     "CSSKeyframesMemoryReduction", base::FEATURE_DISABLED_BY_DEFAULT};
+
+// Kill switch for the new behavior whereby anchors with target=_blank get
+// noopener behavior by default. TODO(crbug.com/898942): Remove in Chrome 95.
+const base::Feature kTargetBlankImpliesNoOpener{
+    "TargetBlankImpliesNoOpener", base::FEATURE_ENABLED_BY_DEFAULT};
 }  // namespace features
 }  // namespace blink
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 01ec6be..8d9de35 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -335,6 +335,8 @@
 BLINK_COMMON_EXPORT extern const base::Feature kLoadingTasksUnfreezable;
 
 BLINK_COMMON_EXPORT extern const base::Feature kCSSKeyframesMemoryReduction;
+
+BLINK_COMMON_EXPORT extern const base::Feature kTargetBlankImpliesNoOpener;
 }  // namespace features
 }  // namespace blink
 
diff --git a/third_party/blink/public/platform/web_runtime_features.h b/third_party/blink/public/platform/web_runtime_features.h
index 241f0fd..7eab62d2 100644
--- a/third_party/blink/public/platform/web_runtime_features.h
+++ b/third_party/blink/public/platform/web_runtime_features.h
@@ -257,6 +257,7 @@
       bool);
 
   BLINK_PLATFORM_EXPORT static void EnableCompositingOptimizations(bool);
+  BLINK_PLATFORM_EXPORT static void EnableTargetBlankImpliesNoOpener(bool);
 
   BLINK_PLATFORM_EXPORT static void EnableParseUrlProtocolHandler(bool);
 
diff --git a/third_party/blink/renderer/core/html/html_anchor_element.cc b/third_party/blink/renderer/core/html/html_anchor_element.cc
index 6a42b83..00bcc41 100644
--- a/third_party/blink/renderer/core/html/html_anchor_element.cc
+++ b/third_party/blink/renderer/core/html/html_anchor_element.cc
@@ -330,12 +330,21 @@
     link_relations_ |= kRelationNoReferrer;
   if (new_link_relations.Contains("noopener"))
     link_relations_ |= kRelationNoOpener;
+  if (new_link_relations.Contains("opener"))
+    link_relations_ |= kRelationOpener;
 }
 
 const AtomicString& HTMLAnchorElement::GetName() const {
   return GetNameAttribute();
 }
 
+const AtomicString& HTMLAnchorElement::GetEffectiveTarget() const {
+  const AtomicString& target = FastGetAttribute(html_names::kTargetAttr);
+  if (!target.IsEmpty())
+    return target;
+  return GetDocument().BaseTarget();
+}
+
 int HTMLAnchorElement::DefaultTabIndex() const {
   return 0;
 }
@@ -525,7 +534,7 @@
 
   request.SetRequestContext(mojom::blink::RequestContextType::HYPERLINK);
   request.SetHasUserGesture(LocalFrame::HasTransientUserActivation(frame));
-  const AtomicString& target = FastGetAttribute(html_names::kTargetAttr);
+  const AtomicString& target = GetEffectiveTarget();
   FrameLoadRequest frame_request(window, request);
   frame_request.SetNavigationPolicy(NavigationPolicyFromEvent(&event));
   frame_request.SetClientRedirectReason(ClientNavigationReason::kAnchorClick);
@@ -533,8 +542,12 @@
     frame_request.SetNoReferrer();
     frame_request.SetNoOpener();
   }
-  if (HasRel(kRelationNoOpener))
+  if (HasRel(kRelationNoOpener) ||
+      (EqualIgnoringASCIICase(target, "_blank") && !HasRel(kRelationOpener) &&
+       RuntimeEnabledFeatures::TargetBlankImpliesNoOpenerEnabled())) {
     frame_request.SetNoOpener();
+  }
+
   frame_request.SetTriggeringEventInfo(
       event.isTrusted() ? TriggeringEventInfo::kFromTrustedEvent
                         : TriggeringEventInfo::kFromUntrustedEvent);
@@ -543,11 +556,7 @@
   frame->MaybeLogAdClickNavigation();
 
   Frame* target_frame =
-      frame->Tree()
-          .FindOrCreateFrameForNavigation(
-              frame_request,
-              target.IsEmpty() ? GetDocument().BaseTarget() : target)
-          .frame;
+      frame->Tree().FindOrCreateFrameForNavigation(frame_request, target).frame;
 
   // If hrefTranslate is enabled and set restrict processing it
   // to same frame or navigations with noopener set.
diff --git a/third_party/blink/renderer/core/html/html_anchor_element.h b/third_party/blink/renderer/core/html/html_anchor_element.h
index 7685ce7..e1478de 100644
--- a/third_party/blink/renderer/core/html/html_anchor_element.h
+++ b/third_party/blink/renderer/core/html/html_anchor_element.h
@@ -58,6 +58,7 @@
   //     RelationTag         = 0x00010000,
   //     RelationUp          = 0x00020000,
   kRelationNoOpener = 0x00040000,
+  kRelationOpener = 0x00080000
 };
 
 class CORE_EXPORT HTMLAnchorElement : public HTMLElement, public DOMURLUtils {
@@ -73,6 +74,10 @@
 
   const AtomicString& GetName() const;
 
+  // Returns the anchor's |target| attribute, unless it is empty, in which case
+  // the BaseTarget from the document is returned.
+  const AtomicString& GetEffectiveTarget() const;
+
   KURL Url() const final;
   void SetURL(const KURL&) final;
 
diff --git a/third_party/blink/renderer/platform/exported/web_runtime_features.cc b/third_party/blink/renderer/platform/exported/web_runtime_features.cc
index 460b477..facdee3 100644
--- a/third_party/blink/renderer/platform/exported/web_runtime_features.cc
+++ b/third_party/blink/renderer/platform/exported/web_runtime_features.cc
@@ -743,4 +743,8 @@
   RuntimeEnabledFeatures::SetWebIDEnabled(enable);
 }
 
+void WebRuntimeFeatures::EnableTargetBlankImpliesNoOpener(bool enable) {
+  RuntimeEnabledFeatures::SetTargetBlankImpliesNoOpenerEnabled(enable);
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index 2a3fd5c..73cfd89 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -1871,6 +1871,11 @@
       depends_on: ["LayoutNG"],
       status: "stable",
     },
+    {
+      // TODO(crbug.com/898942): Remove this in Chrome 95.
+      name: "TargetBlankImpliesNoOpener",
+      status: "stable",
+    },
     // For unit tests.
     {
       name: "TestFeature",
diff --git a/third_party/blink/web_tests/external/wpt/content-security-policy/unsafe-hashes/javascript_src_allowed-href_blank.html b/third_party/blink/web_tests/external/wpt/content-security-policy/unsafe-hashes/javascript_src_allowed-href_blank.html
index d1c2b38..007338b 100644
--- a/third_party/blink/web_tests/external/wpt/content-security-policy/unsafe-hashes/javascript_src_allowed-href_blank.html
+++ b/third_party/blink/web_tests/external/wpt/content-security-policy/unsafe-hashes/javascript_src_allowed-href_blank.html
@@ -13,7 +13,7 @@
 
 <body>
     <div id='log'></div>
-    <a target="_blank" href='javascript:opener.t1.done();' id='test'>
+    <a target="_blank" rel="opener" href='javascript:opener.t1.done();' id='test'>
     <script nonce='abc'>
         var t1 = async_test("Test that the javascript: src is allowed to run");
 
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html b/third_party/blink/web_tests/external/wpt/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html
index 6f43a518..7575c7c 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/windows/auxiliary-browsing-contexts/resources/open-closer.html
@@ -2,7 +2,7 @@
 <meta charset="utf-8">
 <html>
 <body onload="openAuxiliary()">
-<a target="_blank">Open auxiliary context that will close this window (its opener)</a>
+<a rel="opener" target="_blank">Open auxiliary context that will close this window (its opener)</a>
 <script src="/common/PrefixedLocalStorage.js"></script>
 <script>
 function openAuxiliary () {
diff --git a/third_party/blink/web_tests/external/wpt/html/browsers/windows/browsing-context-names/choose-_blank-003.html b/third_party/blink/web_tests/external/wpt/html/browsers/windows/browsing-context-names/choose-_blank-003.html
index 6912300..5571344 100644
--- a/third_party/blink/web_tests/external/wpt/html/browsers/windows/browsing-context-names/choose-_blank-003.html
+++ b/third_party/blink/web_tests/external/wpt/html/browsers/windows/browsing-context-names/choose-_blank-003.html
@@ -11,10 +11,10 @@
 async_test(t => {
   t.add_cleanup(() => prefixedStorage.cleanup());
   prefixedStorage.onSet('hasOpener', t.step_func_done(e => {
-    assert_equals(e.newValue, 'true');
+    assert_equals(e.newValue, 'false');
   }));
   var a = document.getElementsByTagName('a')[0];
   a.href = prefixedStorage.url(a.href);
   a.click();
-}, 'Context created by link targeting "_blank" should retain opener reference');
+}, 'Context created by link targeting "_blank" should not have opener reference');
 </script>
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_helper-2.html b/third_party/blink/web_tests/external/wpt/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_helper-2.html
index 9c393fc..ea28cf5 100644
--- a/third_party/blink/web_tests/external/wpt/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_helper-2.html
+++ b/third_party/blink/web_tests/external/wpt/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_helper-2.html
@@ -19,6 +19,7 @@
         var a = document.createElement("a");
         a.href = location.href;
         a.target = "_blank";
+        a.rel = "opener";
         document.body.appendChild(a);
         a.click();
       }
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener-expected.txt b/third_party/blink/web_tests/external/wpt/html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener-expected.txt
deleted file mode 100644
index 160569fe..0000000
--- a/third_party/blink/web_tests/external/wpt/html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener-expected.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-This is a testharness.js-based test.
-PASS Anchor element with target=_blank with rel=noopener
-PASS Anchor element with target=_blank with rel=opener
-FAIL Anchor element with target=_blank with implicit rel=noopener assert_equals: expected false but got true
-PASS Anchor element with target=_blank with rel=opener+noopener
-PASS Anchor element with target=_blank with rel=noopener+opener
-PASS Anchor element with target=_blank with rel=noreferrer
-PASS Anchor element with target=_blank with rel=opener+noreferrer
-PASS Anchor element with target=_blank with rel=noopener+opener+noreferrer
-PASS Area element with target=_blank with rel=noopener
-PASS Area element with target=_blank with rel=opener
-FAIL Area element with target=_blank with implicit rel=noopener assert_equals: expected false but got true
-PASS Area element with target=_blank with rel=opener+noopener
-PASS Area element with target=_blank with rel=noopener+opener
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener_base-expected.txt b/third_party/blink/web_tests/external/wpt/html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener_base-expected.txt
deleted file mode 100644
index aab46c1..0000000
--- a/third_party/blink/web_tests/external/wpt/html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener_base-expected.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-This is a testharness.js-based test.
-PASS Anchor element with base target=_blank with rel=noopener
-PASS Anchor element with base target=_blank with rel=opener
-FAIL Anchor element with base target=_blank with implicit rel=noopener assert_equals: expected false but got true
-PASS Anchor element with base target=_blank with rel=opener+noopener
-PASS Anchor element with base target=_blank with rel=noopener+opener
-PASS Anchor element with base target=_blank with rel=noreferrer
-PASS Anchor element with base target=_blank with rel=opener+noreferrer
-PASS Anchor element with base target=_blank with rel=noopener+opener+noreferrer
-PASS Area element with base target=_blank with rel=noopener
-PASS Area element with base target=_blank with rel=opener
-FAIL Area element with base target=_blank with implicit rel=noopener assert_equals: expected false but got true
-PASS Area element with base target=_blank with rel=opener+noopener
-PASS Area element with base target=_blank with rel=noopener+opener
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset-expected.txt b/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset-expected.txt
index 2c82186..e3a68d3 100644
--- a/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset-expected.txt
+++ b/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset-expected.txt
@@ -1,5 +1,5 @@
 This tests whether referrer information gets properly set and reset when "noreferrer" links are present. We do the following:
-1. Open a link in a new window: referrer is sent and window.opener is sent.
+1. Open a rel="opener" link in a new window: referrer is sent and window.opener is sent.
 2. Click a rel="noreferrer" link: referrer is null, but window.opener remains set since the link was not opened with target="_blank".
 3. Click a link without rel="noreferrer": referrer is sent, but window.opener is still set.
 Start reset test
diff --git a/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset.html b/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset.html
index 918cdef..7d63d29 100644
--- a/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset.html
+++ b/third_party/blink/web_tests/http/tests/navigation/no-referrer-reset.html
@@ -1,10 +1,10 @@
 <html><body>
 This tests whether referrer information gets properly set and reset when "noreferrer" links are present. We do the following:<br/>
-1. Open a link in a new window: referrer is sent and window.opener is sent.<br/>
+1. Open a rel="opener" link in a new window: referrer is sent and window.opener is sent.<br/>
 2. Click a rel="noreferrer" link: referrer is null, but window.opener remains set since the link was not opened with target="_blank".<br/>
 3. Click a link without rel="noreferrer": referrer is sent, but window.opener is still set.
 <br/>
-<a id="link" href="resources/no-referrer-reset-helper.php" target="_blank">Start reset test</a>
+<a id="link" rel="opener" href="resources/no-referrer-reset-helper.php" target="_blank">Start reset test</a>
 <script>
     window.name = "consoleWindow";
     window.noreferrerStepDone = false;
@@ -18,7 +18,7 @@
     eventSender.mouseMoveTo(target.offsetLeft + 2, target.offsetTop + 2);
     eventSender.mouseDown();
     eventSender.mouseUp();
-    
+
 function log(msg)
 {
     var line = document.createElement('div');
diff --git a/third_party/blink/web_tests/http/tests/security/resources/referrer-policy-redirect-link.html b/third_party/blink/web_tests/http/tests/security/resources/referrer-policy-redirect-link.html
index 1526dbb..b9b4d82 100644
--- a/third_party/blink/web_tests/http/tests/security/resources/referrer-policy-redirect-link.html
+++ b/third_party/blink/web_tests/http/tests/security/resources/referrer-policy-redirect-link.html
@@ -20,7 +20,7 @@
 </script>
 </head>
 <body>
-<a id="link" target="_blank" href="https://127.0.0.1:8443/resources/redirect.php?url=http://127.0.0.1:8000/security/resources/referrer-policy-postmessage.php">If not running in DumpRenderTree, click this link</a>
+<a id="link" rel="opener" target="_blank" href="https://127.0.0.1:8443/resources/redirect.php?url=http://127.0.0.1:8000/security/resources/referrer-policy-postmessage.php">If not running in DumpRenderTree, click this link</a>
 <div id="log"></div>
 </body>
 </html>
diff --git a/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document-unsandboxed.php b/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document-unsandboxed.php
index 2f81ba5..cce3635cae 100644
--- a/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document-unsandboxed.php
+++ b/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document-unsandboxed.php
@@ -8,7 +8,7 @@
     <script src="/resources/testharnessreport.js"></script>
 </head>
 <body>
-    <a target='_blank' href='/security/resources/post-origin-to-opener.html'></a>
+    <a target='_blank' rel="opener" href='/security/resources/post-origin-to-opener.html'></a>
     <script>
         if (window.testRunner) {
             testRunner.setCanOpenWindows();
diff --git a/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document.php b/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document.php
index dd0ec7e..7954603 100644
--- a/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document.php
+++ b/third_party/blink/web_tests/http/tests/security/sandbox-inherit-to-blank-document.php
@@ -8,7 +8,7 @@
     <script src="/resources/testharnessreport.js"></script>
 </head>
 <body>
-    <a target='_blank' href='/security/resources/post-origin-to-opener.html'></a>
+    <a target='_blank' rel="opener" href='/security/resources/post-origin-to-opener.html'></a>
     <script>
         if (window.testRunner) {
             testRunner.setCanOpenWindows();
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 854ee8ac..db4ec0c 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -22459,6 +22459,7 @@
   <int value="799" label="SecurityTokenSessionBehavior"/>
   <int value="800" label="SecurityTokenSessionNotificationSeconds"/>
   <int value="801" label="TosDialogBehavior"/>
+  <int value="802" label="TargetBlankImpliesNoOpener"/>
 </enum>
 
 <enum name="EnterprisePolicyDeviceIdValidity">