diff --git a/CHANGELOG.md b/CHANGELOG.md index a92bb6859b5..0880a2e34c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - Fixed propagation of `esc` key presses closing parent popovers ([#4336](https://github.com/elastic/eui/pull/4336)) - Fixed overwritten `isDisabled` prop on `EuiListGroupItem` `extraAction` config ([#4359](https://github.com/elastic/eui/pull/4359)) - Fixed `inputRef` for `EuiCheckbox` ([#4298](https://github.com/elastic/eui/pull/4298)) +- Limited the links allowed in `EuiMarkdownEditor` to http, https, or starting with a forward slash ([#4362](https://github.com/elastic/eui/pull/4362)) +- Aligned components with an `href` prop to React's practice of disallowing `javascript:` protocols ([#4362](https://github.com/elastic/eui/pull/4362)) **Theme: Amsterdam** diff --git a/package.json b/package.json index 06b06fcd7a4..3a10fa446cb 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "tabbable": "^3.0.0", "text-diff": "^1.0.1", "unified": "^9.2.0", + "url-parse": "^1.4.7", "uuid": "^8.3.0", "vfile": "^4.2.0" }, @@ -108,6 +109,7 @@ "@types/react-router-dom": "^5.1.5", "@types/resize-observer-browser": "^0.1.3", "@types/tabbable": "^3.1.0", + "@types/url-parse": "^1.4.3", "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "^3.10.1", "@typescript-eslint/parser": "^3.10.1", diff --git a/src-docs/src/views/link/link_example.js b/src-docs/src/views/link/link_example.js index 62ca51cf845..468a94648e0 100644 --- a/src-docs/src/views/link/link_example.js +++ b/src-docs/src/views/link/link_example.js @@ -10,6 +10,7 @@ import linkConfig from './playground'; import Link from './link'; import { LinkDisable } from './link_disable'; +import { LinkValidation } from './link_validation'; const linkSource = require('!!raw-loader!./link'); const linkHtml = renderToHtml(Link); @@ -17,6 +18,9 @@ const linkHtml = renderToHtml(Link); const linkDisableSource = require('!!raw-loader!./link_disable'); const linkDisableHtml = renderToHtml(LinkDisable); +const linkValidationSource = require('!!raw-loader!./link_validation'); +const linkValidationHtml = renderToHtml(LinkValidation); + const linkSnippet = [ ` `, @@ -77,6 +81,36 @@ export const LinkExample = { props: { EuiLink }, demo: , }, + { + title: 'Link validation', + source: [ + { + type: GuideSectionTypes.JS, + code: linkValidationSource, + }, + { + type: GuideSectionTypes.HTML, + code: linkValidationHtml, + }, + ], + text: ( +

+ To make links more secure for users, EuiLink and + other components that accept an href prop become + disabled if that href uses the{' '} + javascript: protocol. This helps protect consuming + applications from cross-site scripting (XSS) attacks and mirrors + React's{' '} + + planned behavior + {' '} + to prevent rendering of javascript: links. +

+ ), + demo: , + }, ], playground: linkConfig, }; diff --git a/src-docs/src/views/link/link_validation.js b/src-docs/src/views/link/link_validation.js new file mode 100644 index 00000000000..356a902762c --- /dev/null +++ b/src-docs/src/views/link/link_validation.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { EuiLink } from '../../../../src/components'; + +const urls = [ + 'https://elastic.co', + '//elastic.co', + 'relative/url/somewhere', + 'http://username:password@example.com/', + // eslint-disable-next-line no-script-url + 'javascript:alert()', +]; + +export const LinkValidation = () => { + return ( + <> + {urls.map((url) => ( +
+ + {url} + +
+ ))} + + ); +}; diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx index b8b02947439..957c8dd3d3d 100644 --- a/src/components/badge/badge.tsx +++ b/src/components/badge/badge.tsx @@ -36,6 +36,7 @@ import { import { EuiInnerText } from '../inner_text'; import { EuiIcon, IconColor, IconType } from '../icon'; import { chromaValid, parseColor } from '../color_picker/utils'; +import { validateHref } from '../../services/security/href_validator'; type IconSide = 'left' | 'right'; @@ -136,7 +137,7 @@ export const EuiBadge: FunctionComponent = ({ iconType, iconSide = 'left', className, - isDisabled, + isDisabled: _isDisabled, onClick, iconOnClick, onClickAriaLabel, @@ -150,6 +151,9 @@ export const EuiBadge: FunctionComponent = ({ }) => { checkValidColor(color); + const isHrefValid = !href || validateHref(href); + const isDisabled = _isDisabled || !isHrefValid; + let optionalCustomStyles: object | undefined = style; let textColor = null; // TODO - replace with variable once https://github.com/elastic/eui/issues/2731 is closed @@ -215,6 +219,7 @@ export const EuiBadge: FunctionComponent = ({ 'euiBadge__icon', closeButtonProps && closeButtonProps.className ); + const Element = href && !isDisabled ? 'a' : 'button'; const relObj: { href?: string; diff --git a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap index 08f28f3d581..415efa62d7f 100644 --- a/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap +++ b/src/components/basic_table/__snapshots__/in_memory_table.test.tsx.snap @@ -383,6 +383,7 @@ exports[`EuiInMemoryTable behavior pagination 1`] = ` - - - -