Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] Add jsx-no-script-url to prevent usage of javascript: URLs #2419

Merged
merged 1 commit into from Nov 30, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2807,3 +2807,4 @@ If you're still not using React 15 you can keep the old behavior by setting the
[`static-property-placement`]: docs/rules/static-property-placement.md
[`jsx-curly-newline`]: docs/rules/jsx-curly-newline.md
[`jsx-no-useless-fragment`]: docs/rules/jsx-no-useless-fragment.md
[`jsx-no-script-url`]: docs/rules/jsx-no-script-url.md
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -168,6 +168,7 @@ Enable the rules that you would like to use.
* [react/jsx-no-comment-textnodes](docs/rules/jsx-no-comment-textnodes.md): Prevent comments from being inserted as text nodes
* [react/jsx-no-duplicate-props](docs/rules/jsx-no-duplicate-props.md): Prevent duplicate props in JSX
* [react/jsx-no-literals](docs/rules/jsx-no-literals.md): Prevent usage of unwrapped JSX strings
* [react/jsx-no-script-url](docs/rules/jsx-no-script-url.md): Prevent usage of `javascript:` URLs
* [react/jsx-no-target-blank](docs/rules/jsx-no-target-blank.md): Prevent usage of unsafe `target='_blank'`
* [react/jsx-no-undef](docs/rules/jsx-no-undef.md): Disallow undeclared variables in JSX
* [react/jsx-no-useless-fragment](docs/rules/jsx-no-useless-fragment.md): Disallow unnecessary fragments (fixable)
Expand Down
57 changes: 57 additions & 0 deletions docs/rules/jsx-no-script-url.md
@@ -0,0 +1,57 @@
# Prevent usage of `javascript:` URLs (react/jsx-no-script-url)

**In React 16.9** any URLs starting with `javascript:` [scheme](https://wiki.whatwg.org/wiki/URL_schemes#javascript:_URLs) log a warning.
React considers the pattern as a dangerous attack surface, see [details](https://reactjs.org/blog/2019/08/08/react-v16.9.0.html#deprecating-javascript-urls).
**In a future major release**, React will throw an error if it encounters a `javascript:` URL.

## Rule Details

The following patterns are considered warnings:

```jsx
<a href="javascript:"></a>
<a href="javascript:void(0)"></a>
<a href="j\n\n\na\rv\tascript:"></a>
```

The following patterns are **not** considered warnings:

```jsx
<Foo href="javascript:"></Foo>
ljharb marked this conversation as resolved.
Show resolved Hide resolved
<a href={"javascript:"}></a>
```

## Rule Options
```json
{
"react/jsx-no-script-url": [
"error",
[
sergei-startsev marked this conversation as resolved.
Show resolved Hide resolved
{
"name": "Link",
"props": ["to"]
},
{
"name": "Foo",
"props": ["href", "to"]
}
]
]
}
```

Allows you to indicate a specific list of properties used by a custom component to be checked.

### name
Component name.

### props
List of properties that should be validated.

The following patterns are considered warnings with the options listed above:

```jsx
<Link to="javascript:void(0)"></Link>
<Foo href="javascript:void(0)"></Foo>
<Foo to="javascript:void(0)"></Foo>
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -34,6 +34,7 @@ const allRules = {
'jsx-no-comment-textnodes': require('./lib/rules/jsx-no-comment-textnodes'),
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
'jsx-no-script-url': require('./lib/rules/jsx-no-script-url'),
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
'jsx-no-useless-fragment': require('./lib/rules/jsx-no-useless-fragment'),
'jsx-one-expression-per-line': require('./lib/rules/jsx-one-expression-per-line'),
Expand Down
91 changes: 91 additions & 0 deletions lib/rules/jsx-no-script-url.js
@@ -0,0 +1,91 @@
/**
* @fileoverview Prevent usage of `javascript:` URLs
* @author Sergei Startsev
*/

'use strict';

const docsUrl = require('../util/docsUrl');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

// https://github.com/facebook/react/blob/d0ebde77f6d1232cefc0da184d731943d78e86f2/packages/react-dom/src/shared/sanitizeURL.js#L30
/* eslint-disable-next-line max-len, no-control-regex */
const isJavaScriptProtocol = /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;

function hasJavaScriptProtocol(attr) {
return attr.value.type === 'Literal' &&
isJavaScriptProtocol.test(attr.value.value);
}

function shouldVerifyElement(node, config) {
const name = node.name && node.name.name;
return name === 'a' || config.find(i => i.name === name);
}

function shouldVerifyProp(node, config) {
const name = node.name && node.name.name;
const parentName = node.parent.name && node.parent.name.name;

if (parentName === 'a' && name === 'href') {
return true;
}

const el = config.find(i => i.name === parentName);
if (!el) {
return false;
}

const props = el.props || [];
return node.name && props.indexOf(name) !== -1;
}

module.exports = {
meta: {
docs: {
description: 'Forbid `javascript:` URLs',
category: 'Best Practices',
recommended: false,
url: docsUrl('jsx-no-script-url')
},
schema: [{
type: 'array',
sergei-startsev marked this conversation as resolved.
Show resolved Hide resolved
uniqueItems: true,
items: {
type: 'object',
properties: {
name: {
type: 'string'
},
props: {
type: 'array',
items: {
type: 'string',
uniqueItems: true
}
}
},
required: ['name', 'props'],
additionalProperties: false
sergei-startsev marked this conversation as resolved.
Show resolved Hide resolved
}
}]
},

create(context) {
const config = context.options[0] || [];
return {
JSXAttribute(node) {
const parent = node.parent;
if (shouldVerifyElement(parent, config) && shouldVerifyProp(node, config) && hasJavaScriptProtocol(node)) {
context.report({
node,
message: 'A future version of React will block javascript: URLs as a security precaution. ' +
'Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.'
});
}
}
};
}
};
69 changes: 69 additions & 0 deletions tests/lib/rules/jsx-no-script-url.js
@@ -0,0 +1,69 @@
/**
* @fileoverview Prevent usage of `javascript:` URLs
* @author Sergei Startsev
*/

'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/jsx-no-script-url');

const parserOptions = {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
};

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({parserOptions});
const message = 'A future version of React will block javascript: URLs as a security precaution. ' +
'Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.';
const defaultErrors = [{message}];

ruleTester.run('jsx-no-script-url', rule, {
valid: [
{code: '<a href="https://reactjs.org"></a>'},
{code: '<a href="mailto:foo@bar.com"></a>'},
{code: '<a href="#"></a>'},
{code: '<a href=""></a>'},
{code: '<a name="foo"></a>'},
{code: '<a href={"javascript:"}></a>'},
{code: '<Foo href="javascript:"></Foo>'}
],
invalid: [{
code: '<a href="javascript:"></a>',
errors: defaultErrors
}, {
code: '<a href="javascript:void(0)"></a>',
errors: defaultErrors
}, {
code: '<a href="j\n\n\na\rv\tascript:"></a>',
errors: defaultErrors
}, {
code: '<Foo to="javascript:"></Foo>',
errors: defaultErrors,
options: [[{name: 'Foo', props: ['to', 'href']}]]
}, {
code: '<Foo href="javascript:"></Foo>',
errors: defaultErrors,
options: [[{name: 'Foo', props: ['to', 'href']}]]
}, {
code: `
<div>
<Foo href="javascript:"></Foo>
<Bar link="javascript:"></Bar>
</div>
`,
errors: [{message}, {message}],
options: [[{name: 'Foo', props: ['to', 'href']}, {name: 'Bar', props: ['link']}]]
}]
});