Skip to content

Commit

Permalink
[New] Add jsx-no-script-url to prevent usage of javascript: URLs
Browse files Browse the repository at this point in the history
  • Loading branch information
sergei-startsev authored and ljharb committed Nov 30, 2019
1 parent 221164a commit 2ecdf36
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 0 deletions.
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>
<a href={"javascript:"}></a>
```

## Rule Options
```json
{
"react/jsx-no-script-url": [
"error",
[
{
"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',
uniqueItems: true,
items: {
type: 'object',
properties: {
name: {
type: 'string'
},
props: {
type: 'array',
items: {
type: 'string',
uniqueItems: true
}
}
},
required: ['name', 'props'],
additionalProperties: false
}
}]
},

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']}]]
}]
});

0 comments on commit 2ecdf36

Please sign in to comment.