Skip to content

Commit

Permalink
[New] jsx-no-target-blank: add forms option
Browse files Browse the repository at this point in the history
  • Loading branch information
jaaberg authored and ljharb committed Dec 17, 2017
1 parent a3c6d7e commit 4832801
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 16 deletions.
7 changes: 6 additions & 1 deletion README.md
Expand Up @@ -51,8 +51,13 @@ You should also specify settings that will be shared across all the plugin rules
{"property": "freeze", "object": "Object"},
{"property": "myFavoriteWrapper"}
],
"formComponents": [
// Components used as alternatives to <form> for forms, eg. <Form endpoint={url} />
"CustomForm",
{"name": "Form", "formAttribute": "endpoint"}
]
"linkComponents": [
// Components used as alternatives to <a> for linking, eg. <Link to={ url } />
// Components used as alternatives to <a> for linking, eg. <Link to={url} />
"Hyperlink",
{"name": "Link", "linkAttribute": "to"}
]
Expand Down
35 changes: 29 additions & 6 deletions docs/rules/jsx-no-target-blank.md
@@ -1,25 +1,30 @@
# Prevent usage of unsafe `target='_blank'` (react/jsx-no-target-blank)

When creating a JSX element that has an `a` tag, it is often desired to have
When creating a JSX element that has an `a` tag or a `form` tag, it is often desired to have
the link open in a new tab using the `target='_blank'` attribute. Using this
attribute unaccompanied by `rel='noreferrer noopener'`, however, is a severe
security vulnerability ([see here for more details](https://mathiasbynens.github.io/rel-noopener))
This rules requires that you accompany `target='_blank'` attributes with `rel='noreferrer noopener'`.

## Rule Details

This rule aims to prevent user generated links from creating security vulnerabilities by requiring
`rel='noreferrer noopener'` for external links, and optionally any dynamically generated links.
This rule aims to prevent user generated link hrefs and form actions from creating security vulnerabilities by requiring `rel='noreferrer noopener'` for external link hrefs and form actions, and optionally any dynamically generated link hrefs and form actions.

## Rule Options
```json
...
"react/jsx-no-target-blank": [<enabled>, { "enforceDynamicLinks": <enforce> }]
"react/jsx-no-target-blank": [<enabled>, {
"enforceDynamicLinks": <enforce>,
"links": <boolean>,
"forms": <boolean>,
}]
...
```

* enabled: for enabling the rule. 0=off, 1=warn, 2=error. Defaults to 0.
* enforce: optional string, 'always' or 'never'
* `enforceDynamicLinks` - enforce: optional string, 'always' or 'never'
* `links` - Prevent usage of unsafe `target='_blank'` inside links, defaults to `true`
* `forms` - Prevent usage of unsafe `target='_blank'` inside forms, defaults to `false`

### `enforceDynamicLinks`

Expand Down Expand Up @@ -54,6 +59,19 @@ When {"enforceDynamicLinks": "never"} is set, the following patterns are **not**
var Hello = <a target='_blank' href={dynamicLink}></a>
```

### `links` / `forms`

When option `forms` is set to `true`, the following is considered an error:

```jsx
var Hello = <form target='_blank' action="http://example.com/"></form>
```
When option `links` is set to `true`, the following is considered an error:

```jsx
var Hello = <a target='_blank' href="http://example.com/"></form>
```

### Custom link components

This rule supports the ability to use custom components for links, such as `<Link />` which is popular in libraries like `react-router`, `next.js` and `gatsby`. To enable this, define your custom link components in the global [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) under the `linkComponents` configuration area. Once configured, this rule will check those components as if they were `<a />` elements.
Expand All @@ -74,6 +92,11 @@ var Hello = <Link target="_blank" to="/absolute/path/in/the/host"></Link>
var Hello = <Link />
```

### Custom form components

This rule supports the ability to use custom components for forms. To enable this, define your custom form components in the global [shared settings](https://github.com/yannickcr/eslint-plugin-react/blob/master/README.md#configuration) under the `formComponents` configuration area. Once configured, this rule will check those components as if they were `<form />` elements.


## When Not To Use It

If you do not have any external links, you can disable this rule
If you do not have any external links or forms, you can disable this rule
45 changes: 36 additions & 9 deletions lib/rules/jsx-no-target-blank.js
Expand Up @@ -56,28 +56,55 @@ module.exports = {
properties: {
enforceDynamicLinks: {
enum: ['always', 'never']
},
links: {
type: 'boolean',
default: true
},
forms: {
type: 'boolean',
default: false
}
},
additionalProperties: false
}]
},

create(context) {
const configuration = context.options[0] || {};
const configuration = Object.assign({
links: true,
forms: false
}, context.options[0]);

const enforceDynamicLinks = configuration.enforceDynamicLinks || 'always';
const components = linkComponentsUtil.getLinkComponents(context);
const linkComponents = linkComponentsUtil.getLinkComponents(context);
const formComponents = linkComponentsUtil.getFormComponents(context);

return {
JSXAttribute(node) {
if (!components.has(node.parent.name.name) || !isTargetBlank(node) || hasSecureRel(node.parent)) {
return;
}
const parent = node.parent;
const tagName = parent.name.name;

if (linkComponents.has(tagName)) {
if (!configuration.links || !isTargetBlank(node) || hasSecureRel(parent)) {
return;
}

const linkAttribute = linkComponents.get(tagName);

if (hasExternalLink(parent, linkAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(parent, linkAttribute))) {
context.report(node, 'Using target="_blank" without rel="noopener noreferrer" is a security risk: see https://mathiasbynens.github.io/rel-noopener');
}
} else if (formComponents.has(tagName)) {
if (!configuration.forms || !isTargetBlank(node) || hasSecureRel(parent)) {
return;
}

const linkAttribute = components.get(node.parent.name.name);
const formAttribute = formComponents.get(tagName);

if (hasExternalLink(node.parent, linkAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(node.parent, linkAttribute))) {
context.report(node, 'Using target="_blank" without rel="noopener noreferrer" ' +
'is a security risk: see https://mathiasbynens.github.io/rel-noopener');
if (hasExternalLink(parent, formAttribute) || (enforceDynamicLinks === 'always' && hasDynamicLink(parent, formAttribute))) {
context.report(node, 'Using target="_blank" without rel="noopener noreferrer" is a security risk: see https://mathiasbynens.github.io/rel-noopener');
}
}
}
};
Expand Down
19 changes: 19 additions & 0 deletions lib/util/linkComponents.js
Expand Up @@ -9,6 +9,24 @@
const DEFAULT_LINK_COMPONENTS = ['a'];
const DEFAULT_LINK_ATTRIBUTE = 'href';

/** TODO: type {(string | { name: string, formAttribute: string })[]} */
/** @type {any} */
const DEFAULT_FORM_COMPONENTS = ['form'];
const DEFAULT_FORM_ATTRIBUTE = 'action';

function getFormComponents(context) {
const settings = context.settings || {};
const formComponents = /** @type {typeof DEFAULT_FORM_COMPONENTS} */ (
DEFAULT_FORM_COMPONENTS.concat(settings.formComponents || [])
);
return new Map(formComponents.map((value) => {
if (typeof value === 'string') {
return [value, DEFAULT_FORM_ATTRIBUTE];
}
return [value.name, value.formAttribute];
}));
}

function getLinkComponents(context) {
const settings = context.settings || {};
const linkComponents = /** @type {typeof DEFAULT_LINK_COMPONENTS} */ (
Expand All @@ -23,5 +41,6 @@ function getLinkComponents(context) {
}

module.exports = {
getFormComponents,
getLinkComponents
};
49 changes: 49 additions & 0 deletions tests/lib/rules/jsx-no-target-blank.js
Expand Up @@ -58,8 +58,29 @@ ruleTester.run('jsx-no-target-blank', rule, {
code: '<Link target="_blank" to={ dynamicLink }></Link>',
options: [{enforceDynamicLinks: 'never'}],
settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}}
},
{
code: '<a target="_blank" href="/absolute/path"></a>',
options: [{forms: false}]
},
{
code: '<a target="_blank" href="/absolute/path"></a>',
options: [{forms: false, links: true}]
},
{
code: '<form action="http://example.com" target="_blank"></form>',
options: []
},
{
code: '<form action="http://example.com" target="_blank" rel="noopener noreferrer"></form>',
options: [{forms: true}]
},
{
code: '<form action="http://example.com" target="_blank" rel="noopener noreferrer"></form>',
options: [{forms: true, links: false}]
}
],

invalid: [{
code: '<a target="_blank" href="http://example.com"></a>',
errors: defaultErrors
Expand Down Expand Up @@ -104,5 +125,33 @@ ruleTester.run('jsx-no-target-blank', rule, {
options: [{enforceDynamicLinks: 'always'}],
settings: {linkComponents: {name: 'Link', linkAttribute: 'to'}},
errors: defaultErrors
}, {
code: '<a target="_blank" href="//example.com" rel></a>',
options: [{links: true}],
errors: defaultErrors
}, {
code: '<a target="_blank" href="//example.com" rel></a>',
options: [{links: true, forms: true}],
errors: defaultErrors
}, {
code: '<a target="_blank" href="//example.com" rel></a>',
options: [{links: true, forms: false}],
errors: defaultErrors
}, {
code: '<form method="POST" action="http://example.com" target="_blank"></form>',
options: [{forms: true}],
errors: defaultErrors
}, {
code: '<form method="POST" action="http://example.com" rel="" target="_blank"></form>',
options: [{forms: true}],
errors: defaultErrors
}, {
code: '<form method="POST" action="http://example.com" rel="noopenernoreferrer" target="_blank"></form>',
options: [{forms: true}],
errors: defaultErrors
}, {
code: '<form method="POST" action="http://example.com" rel="noopenernoreferrer" target="_blank"></form>',
options: [{forms: true, links: false}],
errors: defaultErrors
}]
});

0 comments on commit 4832801

Please sign in to comment.