Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ rules in templates can be disabled with eslint directives with mustache or html
| [route-path-style](docs/rules/route-path-style.md) | enforce usage of kebab-case (instead of snake_case or camelCase) in route paths | | | 💡 |
| [routes-segments-snake-case](docs/rules/routes-segments-snake-case.md) | enforce usage of snake_cased dynamic segments in routes | ✅ | | |

### Security

| Name                       | Description | 💼 | 🔧 | 💡 |
| :--------------------------------------------------------------------- | :-------------------------------------------------------------- | :- | :- | :- |
| [template-link-rel-noopener](docs/rules/template-link-rel-noopener.md) | require rel="noopener noreferrer" on links with target="_blank" | | 🔧 | |

### Services

| Name                                      | Description | 💼 | 🔧 | 💡 |
Expand Down
33 changes: 33 additions & 0 deletions docs/rules/template-link-rel-noopener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ember/template-link-rel-noopener

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

When you want to link to an external page from your app, it is very common to use `<a href="url" target="_blank"></a>`
to make the browser open this link in a new tab.

However, this practice has [performance problems](https://jakearchibald.com/2016/performance-benefits-of-rel-noopener/)
and also opens a door to some security attacks because the opened page can redirect the opener app
to a malicious clone to perform phishing on your users.

Adding `rel="noopener noreferrer"` closes that door and avoids javascript in the opened tab to block the main
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)).

## Examples

This rule **forbids** the following:

```hbs
<a href='https://i.seem.secure.com' target='_blank'>I'm a bait</a>
```

This rule **allows** the following:

```hbs
<a href='https://i.seem.secure.com' target='_blank' rel='noopener noreferrer'>I'm a bait</a>
```

## References

- [Link type "noreferrer"](https://html.spec.whatwg.org/multipage/semantics.html#link-type-noreferrer) spec
57 changes: 57 additions & 0 deletions lib/rules/template-link-rel-noopener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'require rel="noopener noreferrer" on links with target="_blank"',
category: 'Security',
strictGjs: true,
strictGts: true,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-link-rel-noopener.md',
},
fixable: 'code',
schema: [],
messages: {
missingRel: 'links with target="_blank" must have rel="noopener noreferrer"',
},
},
create(context) {
return {
GlimmerElementNode(node) {
if (node.tag !== 'a') {
return;
}

const targetAttr = node.attributes?.find((a) => a.name === 'target');
if (!targetAttr?.value || targetAttr.value.type !== 'GlimmerTextNode') {
return;
}
if (targetAttr.value.chars !== '_blank') {
return;
}

const relAttr = node.attributes?.find((a) => a.name === 'rel');
const hasProperRel =
relAttr?.value?.type === 'GlimmerTextNode' &&
/noopener/.test(relAttr.value.chars) &&
/noreferrer/.test(relAttr.value.chars);

if (!hasProperRel) {
context.report({
node: targetAttr,
messageId: 'missingRel',
fix(fixer) {
const sourceCode = context.sourceCode;
const openTag = sourceCode.getText(node).match(/^<a[^>]*/)[0];
const insertPos = node.range[0] + openTag.length;
return fixer.insertTextBeforeRange(
[insertPos, insertPos],
' rel="noopener noreferrer"'
);
},
});
}
},
};
},
};
17 changes: 17 additions & 0 deletions tests/lib/rules/template-link-rel-noopener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const rule = require('../../../lib/rules/template-link-rel-noopener');
const RuleTester = require('eslint').RuleTester;

const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
ruleTester.run('template-link-rel-noopener', rule, {
valid: ['<template><a href="/" target="_blank" rel="noopener noreferrer">Link</a></template>'],
invalid: [
{
code: '<template><a href="/" target="_blank">Link</a></template>',
output: '<template><a href="/" target="_blank" rel="noopener noreferrer">Link</a></template>',
errors: [{ messageId: 'missingRel' }],
},
],
});
Loading