Skip to content

Commit 133830f

Browse files
committed
Extract rule: template-link-rel-noopener
1 parent 5d2ad94 commit 133830f

4 files changed

Lines changed: 113 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,12 @@ rules in templates can be disabled with eslint directives with mustache or html
304304
| [route-path-style](docs/rules/route-path-style.md) | enforce usage of kebab-case (instead of snake_case or camelCase) in route paths | | | 💡 |
305305
| [routes-segments-snake-case](docs/rules/routes-segments-snake-case.md) | enforce usage of snake_cased dynamic segments in routes || | |
306306

307+
### Security
308+
309+
| Name                       | Description | 💼 | 🔧 | 💡 |
310+
| :--------------------------------------------------------------------- | :-------------------------------------------------------------- | :- | :- | :- |
311+
| [template-link-rel-noopener](docs/rules/template-link-rel-noopener.md) | require rel="noopener noreferrer" on links with target="_blank" | | 🔧 | |
312+
307313
### Services
308314

309315
| Name                                      | Description | 💼 | 🔧 | 💡 |
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# ember/template-link-rel-noopener
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
When you want to link to an external page from your app, it is very common to use `<a href="url" target="_blank"></a>`
8+
to make the browser open this link in a new tab.
9+
10+
However, this practice has [performance problems](https://jakearchibald.com/2016/performance-benefits-of-rel-noopener/)
11+
and also opens a door to some security attacks because the opened page can redirect the opener app
12+
to a malicious clone to perform phishing on your users.
13+
14+
Adding `rel="noopener noreferrer"` closes that door and avoids javascript in the opened tab to block the main
15+
thread in the opener. Also note that Firefox versions prior 52 do not implement `noopener`, so `rel="noreferrer"` should be used instead ([see Firefox issue](https://bugzilla.mozilla.org/show_bug.cgi?id=1222516)).
16+
17+
## Examples
18+
19+
This rule **forbids** the following:
20+
21+
```hbs
22+
<a href='https://i.seem.secure.com' target='_blank'>I'm a bait</a>
23+
```
24+
25+
This rule **allows** the following:
26+
27+
```hbs
28+
<a href='https://i.seem.secure.com' target='_blank' rel='noopener noreferrer'>I'm a bait</a>
29+
```
30+
31+
## References
32+
33+
- [Link type "noreferrer"](https://html.spec.whatwg.org/multipage/semantics.html#link-type-noreferrer) spec
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'require rel="noopener noreferrer" on links with target="_blank"',
7+
category: 'Security',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-link-rel-noopener.md',
11+
},
12+
fixable: 'code',
13+
schema: [],
14+
messages: {
15+
missingRel: 'links with target="_blank" must have rel="noopener noreferrer"',
16+
},
17+
},
18+
create(context) {
19+
return {
20+
GlimmerElementNode(node) {
21+
if (node.tag !== 'a') {
22+
return;
23+
}
24+
25+
const targetAttr = node.attributes?.find((a) => a.name === 'target');
26+
if (!targetAttr?.value || targetAttr.value.type !== 'GlimmerTextNode') {
27+
return;
28+
}
29+
if (targetAttr.value.chars !== '_blank') {
30+
return;
31+
}
32+
33+
const relAttr = node.attributes?.find((a) => a.name === 'rel');
34+
const hasProperRel =
35+
relAttr?.value?.type === 'GlimmerTextNode' &&
36+
/noopener/.test(relAttr.value.chars) &&
37+
/noreferrer/.test(relAttr.value.chars);
38+
39+
if (!hasProperRel) {
40+
context.report({
41+
node: targetAttr,
42+
messageId: 'missingRel',
43+
fix(fixer) {
44+
const sourceCode = context.sourceCode;
45+
const openTag = sourceCode.getText(node).match(/^<a[^>]*/)[0];
46+
const insertPos = node.range[0] + openTag.length;
47+
return fixer.insertTextBeforeRange(
48+
[insertPos, insertPos],
49+
' rel="noopener noreferrer"'
50+
);
51+
},
52+
});
53+
}
54+
},
55+
};
56+
},
57+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const rule = require('../../../lib/rules/template-link-rel-noopener');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
ruleTester.run('template-link-rel-noopener', rule, {
9+
valid: ['<template><a href="/" target="_blank" rel="noopener noreferrer">Link</a></template>'],
10+
invalid: [
11+
{
12+
code: '<template><a href="/" target="_blank">Link</a></template>',
13+
output: '<template><a href="/" target="_blank" rel="noopener noreferrer">Link</a></template>',
14+
errors: [{ messageId: 'missingRel' }],
15+
},
16+
],
17+
});

0 commit comments

Comments
 (0)