Skip to content

Commit 8d3c99a

Browse files
authored
New: add vue/no-template-no-target-blank rule (#1086)
* New: add `vue/no-template-no-target-blank` rule * Fix document for no-template-target-blank
1 parent 7609be6 commit 8d3c99a

File tree

5 files changed

+280
-0
lines changed

5 files changed

+280
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ For example:
274274
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
275275
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
276276
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
277+
| [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | |
277278
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
278279
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
279280
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |

Diff for: docs/rules/no-template-target-blank.md

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-template-target-blank
5+
description: disallow target="_blank" attribute without rel="noopener noreferrer"
6+
---
7+
# vue/no-template-target-blank
8+
> disallow target="_blank" attribute without rel="noopener noreferrer"
9+
10+
## :book: Rule Details
11+
12+
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/)).
13+
14+
<eslint-code-block :rules="{'vue/no-template-target-blank': ['error']}">
15+
16+
```vue
17+
<template>
18+
<!-- ✓ Good -->
19+
<a link="http://example.com" target="_blank" rel="noopener noreferrer">link</a>
20+
21+
<!-- ✗ BAD -->
22+
<a link="http://example.com" target="_blank" >link</a>
23+
</temlate>
24+
```
25+
26+
## :wrench: Options
27+
28+
```json
29+
{
30+
"vue/no-template-target-blank": ["error", {
31+
"allowReferrer": true,
32+
"enforceDynamicLinks": "always"
33+
}]
34+
}
35+
```
36+
37+
- `allowReferrer` ... If `true`, does not require noreferrer.default `false`
38+
- `enforceDynamicLinks ("always" | "never")` ... If `always`, enforces the rule if the href is a dynamic link. default `always`.
39+
40+
### `{ allowReferrer: false }` (default)
41+
42+
<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: false }]}">
43+
44+
```vue
45+
<template>
46+
<!-- ✓ Good -->
47+
<a link="http://example.com" target="_blank" rel="noopener noreferrer">link</a>
48+
49+
<!-- ✗ BAD -->
50+
<a link="http://example.com" target="_blank" rel="noopener">link</a>
51+
</temlate>
52+
```
53+
54+
### `{ allowReferrer: true }`
55+
56+
<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { allowReferrer: true }]}">
57+
58+
```vue
59+
<template>
60+
<!-- ✓ Good -->
61+
<a link="http://example.com" target="_blank" rel="noopener">link</a>
62+
63+
<!-- ✗ BAD -->
64+
<a link="http://example.com" target="_blank" >link</a>
65+
</temlate>
66+
```
67+
68+
### `{ "enforceDynamicLinks": "always" }` (default)
69+
70+
<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
71+
72+
```vue
73+
<template>
74+
<!-- ✓ Good -->
75+
<a :link="link" target="_blank" rel="noopener noreferrer">link</a>
76+
77+
<!-- ✗ BAD -->
78+
<a :link="link" target="_blank">link</a>
79+
</temlate>
80+
```
81+
82+
### `{ "enforceDynamicLinks": "never" }`
83+
84+
<eslint-code-block :rules="{'vue/no-template-target-blank': ['error', { enforceDynamicLinks: 'never' }]}">
85+
86+
```vue
87+
<template>
88+
<!-- ✓ Good -->
89+
<a :link="link" target="_blank">link</a>
90+
91+
<!-- ✗ BAD -->
92+
<a link="http://example.com" target="_blank" >link</a>
93+
</temlate>
94+
```
95+
96+
## :mag: Implementation
97+
98+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-template-target-blank.js)
99+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-template-target-blank.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ module.exports = {
6666
'no-static-inline-styles': require('./rules/no-static-inline-styles'),
6767
'no-template-key': require('./rules/no-template-key'),
6868
'no-template-shadow': require('./rules/no-template-shadow'),
69+
'no-template-target-blank': require('./rules/no-template-target-blank'),
6970
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
7071
'no-unsupported-features': require('./rules/no-unsupported-features'),
7172
'no-unused-components': require('./rules/no-unused-components'),

Diff for: lib/rules/no-template-target-blank.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* @fileoverview disallow target="_blank" attribute without rel="noopener noreferrer"
3+
* @author Sosukesuzuki
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// ------------------------------------------------------------------------------
14+
// Helpers
15+
// ------------------------------------------------------------------------------
16+
function isTargetBlank (node) {
17+
return node.key &&
18+
node.key.name === 'target' &&
19+
node.value &&
20+
node.value.value === '_blank'
21+
}
22+
23+
function hasSecureRel (node, allowReferrer) {
24+
return node.attributes.some(attr => {
25+
if (attr.key && attr.key.name === 'rel') {
26+
const tags = attr.value && attr.value.value.toLowerCase().split(' ')
27+
return tags &&
28+
tags.includes('noopener') &&
29+
(allowReferrer || tags.includes('noreferrer'))
30+
} else {
31+
return false
32+
}
33+
})
34+
}
35+
36+
function hasExternalLink (node) {
37+
return node.attributes.some(attr =>
38+
attr.key &&
39+
attr.key.name === 'href' &&
40+
attr.value && /^(?:\w+:|\/\/)/.test(attr.value.value)
41+
)
42+
}
43+
44+
function hasDynamicLink (node) {
45+
return node.attributes.some(attr =>
46+
attr.key &&
47+
attr.key.type === 'VDirectiveKey' &&
48+
attr.key.name &&
49+
attr.key.name.name === 'bind' &&
50+
attr.key.argument &&
51+
attr.key.argument.name === 'href'
52+
)
53+
}
54+
55+
// ------------------------------------------------------------------------------
56+
// Rule Definition
57+
// ------------------------------------------------------------------------------
58+
59+
module.exports = {
60+
meta: {
61+
type: 'problem',
62+
docs: {
63+
description:
64+
'disallow target="_blank" attribute without rel="noopener noreferrer"',
65+
categories: undefined,
66+
url: 'https://eslint.vuejs.org/rules/no-template-target-blank.html'
67+
},
68+
schema: [{
69+
type: 'object',
70+
properties: {
71+
allowReferrer: {
72+
type: 'boolean'
73+
},
74+
enforceDynamicLinks: {
75+
enum: ['always', 'never']
76+
}
77+
},
78+
additionalProperties: false
79+
}]
80+
},
81+
82+
/**
83+
* Creates AST event handlers for no-template-target-blank
84+
*
85+
* @param {RuleContext} context - The rule context.
86+
* @returns {Object} AST event handlers.
87+
*/
88+
create (context) {
89+
const configuration = context.options[0] || {}
90+
const allowReferrer = configuration.allowReferrer || false
91+
const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always'
92+
93+
return utils.defineTemplateBodyVisitor(context, {
94+
'VAttribute' (node) {
95+
if (!isTargetBlank(node) || hasSecureRel(node.parent, allowReferrer)) {
96+
return
97+
}
98+
99+
const hasDangerHref = hasExternalLink(node.parent) ||
100+
(enforceDynamicLinks === 'always' && hasDynamicLink(node.parent))
101+
102+
if (hasDangerHref) {
103+
context.report({
104+
node,
105+
message: 'Using target="_blank" without rel="noopener noreferrer" is a security risk.'
106+
})
107+
}
108+
}
109+
})
110+
}
111+
}

Diff for: tests/lib/rules/no-template-target-blank.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @fileoverview disallow target="_blank" attribute without rel="noopener noreferrer"
3+
* @author Sosukesuzuki
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/no-template-target-blank')
12+
13+
const RuleTester = require('eslint').RuleTester
14+
15+
// ------------------------------------------------------------------------------
16+
// Tests
17+
// ------------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester({
20+
parser: require.resolve('vue-eslint-parser'),
21+
parserOptions: { ecmaVersion: 2015 }
22+
})
23+
24+
ruleTester.run('no-template-target-blank', rule, {
25+
valid: [
26+
{ code: '<template><a>link</a></template>' },
27+
{ code: '<template><a attr>link</a></template>' },
28+
{ code: '<template><a target>link</a></template>' },
29+
{ code: '<template><a href="https://eslint.vuejs.org">link</a></template>' },
30+
{ code: '<template><a :href="link">link</a></template>' },
31+
{ code: '<template><a :href="link" target="_blank" rel="noopener noreferrer">link</a></template>' },
32+
{ code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener noreferrer">link</a></template>' },
33+
{
34+
code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener">link</a></template>',
35+
options: [{ allowReferrer: true }]
36+
},
37+
{ code: '<template><a href="/foo" target="_blank">link</a></template>' },
38+
{ code: '<template><a href="/foo" target="_blank" rel="noopener noreferrer">link</a></template>' },
39+
{ code: '<template><a href="foo/bar" target="_blank">link</a></template>' },
40+
{ code: '<template><a href="foo/bar" target="_blank" rel="noopener noreferrer">link</a></template>' },
41+
{
42+
code: '<template><a :href="link" target="_blank">link</a></template>',
43+
options: [{ enforceDynamicLinks: 'never' }]
44+
}
45+
],
46+
invalid: [
47+
{
48+
code: '<template><a href="https://eslint.vuejs.org" target="_blank">link</a></template>',
49+
errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
50+
},
51+
{
52+
code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopenernoreferrer">link</a></template>',
53+
errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
54+
},
55+
{
56+
code: '<template><a :href="link" target="_blank" rel=3>link</a></template>',
57+
errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
58+
},
59+
{
60+
code: '<template><a :href="link" target="_blank">link</a></template>',
61+
errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
62+
},
63+
{
64+
code: '<template><a href="https://eslint.vuejs.org" target="_blank" rel="noopener">link</a></template>',
65+
errors: ['Using target="_blank" without rel="noopener noreferrer" is a security risk.']
66+
}
67+
]
68+
})

0 commit comments

Comments
 (0)