Repository: vuejs/eslint-plugin-vue (JavaScript)

Issue:
Rule Proposal: template-no-target-blank
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**Please describe what the rule should do:**
<!-- A clear and concise description of what the rule should do. -->
This rule should warn when `rel='noreferrer noopener'` does not exist in attributes of an anchor tag with `target="_blank"`.

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
- [ ] Enforces code style
- [x] Warns about a potential error
- [ ] Suggests an alternate way of doing something
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**
```vue
<template>
  <a href="http://example.com" target="_blank">link</a>
</template>
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
ref: https://developers.google.com/web/tools/lighthouse/audits/noopener?hl=en
A rule does exist for this problem in [eslint-plugin-react](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-target-blank.md)


Discussion:
To any maintainer. Can I work on this?
Thank you for this rule proposal.

I think it is good to add this rule to `eslint-plugin-vue`.
I'm not familiar with [this vulnerability](https://mathiasbynens.github.io/rel-noopener/), so I'm not sure about the rule details.



Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e184873e5..ad720cb3f 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -272,6 +272,7 @@ For example:
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
+| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
diff --git a/docs/rules/no-template-target-blank.md b/docs/rules/no-template-target-blank.md
new file mode 100644
index 000000000..67dfcc995
--- /dev/null
+++ b/docs/rules/no-template-target-blank.md
@@ -0,0 +1,99 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-template-target-blank
+description: disallow target="_blank" attribute without rel="noopener noreferrer"
+---
+# vue/no-template-target-blank
+> disallow target="_blank" attribute without rel="noopener noreferrer"
+
+## :book: Rule Details
+
+This rule disallows using `target="_blank"` attribute without `rel="noopener noreferrer"` to avoid a security vulnerability([see here for more details](https://mathiasbynens.github.io/rel-noopener/)).
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a link="http://example.com" target="_blank" rel="noopener noreferrer">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" >link</a>
+</temlate>
+```
+
+## :wrench: Options
+
+```json
+{
+  "vue/no-template-target-blank": ["error", {
+    "allowReferrer": true,
+    "enforceDynamicLinks": "always"
+  }]
+}
+```
+
+- `allowReferrer` ... If `true`, does not require noreferrer.default `false`
+- `enforceDynamicLinks ("always" | "never")` ... If `always`, enforces the rule if the href is a dynamic link. default `always`.
+
+### `{ allowReferrer: false }` (default)
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: false }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a link="http://example.com" target="_blank" rel="noopener noreferrer">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" rel="noopener">link</a>
+</temlate>
+```
+
+### `{ allowReferrer: true }`
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: true }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a link="http://example.com" target="_blank" rel="noopener">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" >link</a>
+</temlate>
+```
+
+### `{ "enforceDynamicLinks": "always" }` (default)
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a :link="link" target="_blank" rel="noopener noreferrer">link</a>
+
+  <!-- ✗ BAD -->
+  <a :link="link" target="_blank">link</a>
+</temlate>
+```
+
+### `{ "enforceDynamicLinks": "never" }`
+
+<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
+
+```vue
+<template>
+  <!-- ✓ Good -->
+  <a :link="link" target="_blank">link</a>
+
+  <!-- ✗ BAD -->
+  <a link="http://example.com" target="_blank" >link</a>
+</temlate>
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-template-target-blank.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-template-target-blank.js)
diff --git a/lib/index.js b/lib/index.js
index 10e2e8766..5cd804500 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -64,6 +64,7 @@ module.exports = {
     'no-static-inline-styles': require('./rules/no-static-inline-styles'),
     'no-template-key': require('./rules/no-template-key'),
     'no-template-shadow': require('./rules/no-template-shadow'),
+    'no-template-target-blank': require('./rules/no-template-target-blank'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
     'no-unsupported-features': require('./rules/no-unsupported-features'),
     'no-unused-components': require('./rules/no-unused-components'),
diff --git a/lib/rules/no-template-target-blank.js b/lib/rules/no-template-target-blank.js
new file mode 100644
index 000000000..bf43e19e2
--- /dev/null
+++ b/lib/rules/no-template-target-blank.js
@@ -0,0 +1,111 @@
+/**
+ * @fileoverview disallow target="_blank" attribute without rel="noopener noreferrer"
+ * @author Sosukesuzuki
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+function isTargetBlank (node) {
+  return node.key &&
+    node.key.name === 'target' &&
+    node.value &&
+    node.value.value === '_blank'
+}
+
+function hasSecureRel (node, allowReferrer) {
+  return node.attributes.some(attr => {
+    if (attr.key && attr.key.name === 'rel') {
+      const tags = attr.value && attr.value.value.toLowerCase().split(' ')
+      return tags &&
+        tags.includes('noopener') &&
+        (allowReferrer || tags.includes('noreferrer'))
+    } else {
+      return false
+    }
+  })
+}
+
+function hasExternalLink (node) {
+  return node.attributes.some(attr =>
+    attr.key &&
+    attr.key.name === 'href' &&
+    attr.value && /^(?:\w+:|\/\/)/.test(attr.value.value)
+  )
+}
+
+function hasDynamicLink (node) {
+  return node.attributes.some(attr =>
+    attr.key &&
+    attr.key.type === 'VDirectiveKey' &&
+    attr.key.name &&
+    attr.key.name.name === 'bind' &&
+    attr.key.argument &&
+    attr.key.argument.name === 'href'
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow target="_blank" attribute without rel="noopener noreferrer"',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-template-target-blank.html'
+    },
+    schema: [{
+      type: 'object',
+      properties: {
+        allowReferrer: {
+          type: 'boolean'
+        },
+        enforceDynamicLinks: {
+          enum: ['always', 'never']
+        }
+      },
+      additionalProperties: false
+    }]
+  },
+
+  /**
+   * Creates AST event handlers for no-template-target-blank
+   *
+   * @param {RuleContext} context - The rule context.
+   * @returns {Object} AST event handlers.
+   */
+  create (context) {
+    const configuration = context.options[0] || {}
+    const allowReferrer = configuration.allowReferrer || false
+    const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always'
+
+    return utils.defineTemplateBodyVisitor(context, {
+      'VAttribute' (node) {
+        if (!isTargetBlank(node) || hasSecureRel(node.parent, allowReferrer)) {
+          return
+        }
+
+        const hasDangerHref = hasExternalLink(node.parent) ||
+          (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))
+
+        if (hasDangerHref) {
+          context.report({
+            node,
+            message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk.'
+          })
+        }
+      }
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-template-target-blank.js b/tests/lib/rules/no-template-target-blank.js
new file mode 100644
index 000000000..41b5c1c4b
--- /dev/null
+++ b/tests/lib/rules/no-template-target-blank.js
@@ -0,0 +1,68 @@
+/**
+ * @fileoverview disallow target="_blank" attribute without rel="noopener noreferrer"
+ * @author Sosukesuzuki
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-template-target-blank')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('no-template-target-blank', rule, {
+  valid: [
+    { code: '<template><a>link</a></template>' },
+    { code: '<template><a attr>link</a></template>' },
+    { code: '<template><a target>link</a></template>' },
+    { code: '<template><a href="https://eslint.vuejs.org">link</a></template>' },
+    { code: '<template><a :href="link">link</a></template>' },
+    { code: '<template><a :href="link" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    { code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener">link</a></template>',
+      options: [{ allowReferrer: true }]
+    },
+    { code: '<template><a href="/foo" target="_blank">link</a></template>' },
+    { code: '<template><a href="/foo" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    { code: '<template><a href="foo/bar" target="_blank">link</a></template>' },
+    { code: '<template><a href="foo/bar" target="_blank" rel="noopener noreferrer">link</a></template>' },
+    {
+      code: '<template><a :href="link" target="_blank">link</a></template>',
+      options: [{ enforceDynamicLinks: 'never' }]
+    }
+  ],
+  invalid: [
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopenernoreferrer">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a :href="link" target="_blank" rel=3>link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a :href="link" target="_blank">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    },
+    {
+      code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener">link</a></template>',
+      errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
+    }
+  ]
+})