diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79f0bfec7d..ff3caeb4d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2754,3 +2754,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
diff --git a/README.md b/README.md
index 8629db136a..eba722f89f 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/docs/rules/jsx-no-script-url.md b/docs/rules/jsx-no-script-url.md
new file mode 100644
index 0000000000..f3e1247bcf
--- /dev/null
+++ b/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
+
+
+
+```
+
+The following patterns are **not** considered warnings:
+
+```jsx
+
+
+```
+
+## 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
+
+
+
+```
diff --git a/index.js b/index.js
index c9511bb1e3..6db76d0aee 100644
--- a/index.js
+++ b/index.js
@@ -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'),
diff --git a/lib/rules/jsx-no-script-url.js b/lib/rules/jsx-no-script-url.js
new file mode 100644
index 0000000000..0bbce3d8cc
--- /dev/null
+++ b/lib/rules/jsx-no-script-url.js
@@ -0,0 +1,90 @@
+/**
+ * @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);
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Forbid `javascript:` URLs',
+ category: 'Best Practices',
+ recommended: false,
+ url: docsUrl('jsx-no-script-url')
+ },
+ schema: [{
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string'
+ },
+ props: {
+ type: 'array',
+ items: {
+ type: 'string',
+ uniqueItems: true
+ }
+ }
+ },
+ required: ['name', 'props'],
+ additionalProperties: false
+ }
+ }]
+ },
+
+ create(context) {
+ const configuration = context.options[0] || [];
+ const elements = configuration.map(i => i.name);
+
+ function shouldVerifyElement(node) {
+ const name = node.name && node.name.name;
+ return name === 'a' || elements.indexOf(name) !== -1;
+ }
+
+ function shouldVerifyProp(node) {
+ const name = node.name && node.name.name;
+ const parentName = node.parent.name && node.parent.name.name;
+
+ if (parentName === 'a' && name === 'href') {
+ return true;
+ }
+
+ if (elements.indexOf(parentName) === -1) {
+ return false;
+ }
+
+ const el = configuration.find(i => i.name === parentName);
+ const props = el && el.props || [];
+
+ return node.name && props.indexOf(name) !== -1;
+ }
+
+ return {
+ JSXAttribute(node) {
+ const parent = node.parent;
+ if (shouldVerifyElement(parent) && shouldVerifyProp(node) && hasJavaScriptProtocol(node)) {
+ context.report(node, '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.');
+ }
+ }
+ };
+ }
+};
diff --git a/tests/lib/rules/jsx-no-script-url.js b/tests/lib/rules/jsx-no-script-url.js
new file mode 100644
index 0000000000..edb800aa42
--- /dev/null
+++ b/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: ''},
+ {code: ''},
+ {code: ''},
+ {code: ''},
+ {code: ''},
+ {code: ''},
+ {code: ''}
+ ],
+ invalid: [{
+ code: '',
+ errors: defaultErrors
+ }, {
+ code: '',
+ errors: defaultErrors
+ }, {
+ code: '',
+ errors: defaultErrors
+ }, {
+ code: '',
+ errors: defaultErrors,
+ options: [[{name: 'Foo', props: ['to', 'href']}]]
+ }, {
+ code: '',
+ errors: defaultErrors,
+ options: [[{name: 'Foo', props: ['to', 'href']}]]
+ }, {
+ code: `
+
+
+
+
+ `,
+ errors: [{message}, {message}],
+ options: [[{name: 'Foo', props: ['to', 'href']}, {name: 'Bar', props: ['link']}]]
+ }]
+});