Skip to content

Commit f9636da

Browse files
committed
⭐ new(rule): add no-v-html rule
1 parent 7612dfd commit f9636da

File tree

6 files changed

+159
-2
lines changed

6 files changed

+159
-2
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
| Rule ID | Description | |
99
|:--------|:------------|:---|
1010
| [vue-i18n/<wbr>no-missing-keys](./no-missing-keys.html) | disallow missing locale message key at localization methods | :star: |
11+
| [vue-i18n/<wbr>no-v-html](./no-v-html.html) | disallow use of localization methods on v-html to prevent XSS attack | :star: |
1112

1213
## Best Practices
1314

Diff for: docs/rules/no-v-html.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# vue-i18n/no-v-html
2+
3+
> disallow use of localization methods on v-html to prevent XSS attack
4+
5+
- :star: The `"extends": "plugin:vue-i18n/recommended"` property in a configuration file enables this rule.
6+
7+
This rule reports all uses of localization methods on `v-html` directive in order to reduce the risk of injecting potentially unsafe / unescaped html into the browser leading to Cross-Site Scripting (XSS) attacks.
8+
9+
## :book: Rule Details
10+
11+
You can be detected with this rule the following:
12+
13+
- `$t`
14+
- `t`
15+
- `$tc`
16+
- `tc`
17+
18+
:-1: Examples of **incorrect** code for this rule:
19+
20+
locale messages:
21+
```json
22+
{
23+
"term": "<p>I accept xxx <a href=\"\/term\">Terms of Service Agreement</a></p>"
24+
}
25+
```
26+
27+
localization codes:
28+
29+
```vue
30+
<template>
31+
<div class="app">
32+
<!-- ✗ BAD -->
33+
<p v-html="$t('term')"></p>
34+
</div>
35+
</template>
36+
```
37+
38+
:+1: Examples of **correct** code for this rule:
39+
40+
locale messages:
41+
```json
42+
{
43+
"tos": "Term of Service",
44+
"term": "I accept xxx {0}."
45+
}
46+
```
47+
48+
localization codes:
49+
50+
```vue
51+
<template>
52+
<div class="app">
53+
<!-- ✗ GOOD -->
54+
<i18n path="term" tag="label" for="tos">
55+
<a :href="url" target="_blank">{{ $t('tos') }}</a>
56+
</i18n>
57+
</div>
58+
</template>
59+
```
60+
61+
## :mute: When Not To Use It
62+
63+
If you are certain the content passed to `v-html` is sanitized HTML you can disable this rule.
64+
65+
## :books: Further reading
66+
67+
- [XSS in Vue.js](https://blog.sqreen.io/xss-in-vue-js/)

Diff for: lib/configs/recommended.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
module.exports = {
55
plugins: ['vue-i18n'],
66
rules: {
7-
'vue-i18n/no-missing-keys': 'error'
7+
'vue-i18n/no-missing-keys': 'error',
8+
'vue-i18n/no-v-html': 'error'
89
}
910
}

Diff for: lib/rules.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
module.exports = {
55
'no-dynamic-keys': require('./rules/no-dynamic-keys'),
66
'no-missing-keys': require('./rules/no-missing-keys'),
7-
'no-unused-keys': require('./rules/no-unused-keys')
7+
'no-unused-keys': require('./rules/no-unused-keys'),
8+
'no-v-html': require('./rules/no-v-html')
89
}

Diff for: lib/rules/no-v-html.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const { defineTemplateBodyVisitor } = require('../utils/index')
7+
8+
function checkDirective (context, node) {
9+
if ((node.value && node.value.type === 'VExpressionContainer') &&
10+
(node.value.expression && node.value.expression.type === 'CallExpression')) {
11+
const expressionNode = node.value.expression
12+
const funcName = (expressionNode.callee.type === 'MemberExpression' && expressionNode.callee.property.name) || expressionNode.callee.name
13+
if (!/^(\$t|t|\$tc|tc)$/.test(funcName) || !expressionNode.arguments || !expressionNode.arguments.length) {
14+
return
15+
}
16+
context.report({
17+
node,
18+
message: `Using ${funcName} on 'v-html' directive can lead to XSS attack.`
19+
})
20+
}
21+
}
22+
23+
function create (context) {
24+
return defineTemplateBodyVisitor(context, {
25+
"VAttribute[directive=true][key.name='html']" (node) {
26+
checkDirective(context, node)
27+
},
28+
29+
"VAttribute[directive=true][key.name.name='html']" (node) {
30+
checkDirective(context, node)
31+
}
32+
})
33+
}
34+
35+
module.exports = {
36+
meta: {
37+
type: 'suggestion',
38+
docs: {
39+
description: 'disallow use of localization methods on v-html to prevent XSS attack',
40+
category: 'Possible Errors',
41+
recommended: true
42+
},
43+
fixable: null,
44+
schema: []
45+
},
46+
create
47+
}

Diff for: tests/lib/rules/no-v-html.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @author kazuya kawaguchi (a.k.a. kazupon)
3+
*/
4+
'use strict'
5+
6+
const RuleTester = require('eslint').RuleTester
7+
const rule = require('../../../lib/rules/no-v-html')
8+
9+
const tester = new RuleTester({
10+
parser: 'vue-eslint-parser',
11+
parserOptions: { ecmaVersion: 2015 }
12+
})
13+
14+
tester.run('no-v-html', rule, {
15+
valid: [{
16+
code: `<template>
17+
<div class="app">
18+
<i18n path="term" tag="label" for="tos">
19+
<a :href="url" target="_blank">{{ $t('tos') }}</a>
20+
</i18n>
21+
</div>
22+
</template>`
23+
}],
24+
25+
invalid: [{
26+
code: `<template>
27+
<p v-html="$t('hello')"></p>
28+
</template>`,
29+
errors: [
30+
`Using $t on 'v-html' directive can lead to XSS attack.`
31+
]
32+
}, {
33+
code: `<template>
34+
<p v-html="this.t('hello')"></p>
35+
</template>`,
36+
errors: [
37+
`Using t on 'v-html' directive can lead to XSS attack.`
38+
]
39+
}]
40+
})

0 commit comments

Comments
 (0)