Skip to content

Commit 47e869e

Browse files
chandlerprallConstance
andauthored
[EuiMarkdown] Allow mailto: links by default & allow customization of link validation (#5790)
* Transform mailto: links in markdown to just the target address, as we unlink those values for security reasons * documentation * changelog * corrected changelog entry filename * Lint issues * Refactored markdownLinkValidator to be configurable * documentation, documentation, documentation * PR feedback * enable mailto: links by default in markdown * updated comment * test to verify mailto protocol is supported * pr feedback * [PR feedback] Changelog Co-authored-by: Constance <[email protected]>
1 parent e0afbf4 commit 47e869e

File tree

12 files changed

+406
-31
lines changed

12 files changed

+406
-31
lines changed

src-docs/src/views/markdown_editor/mardown_format_example.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
EuiMarkdownFormat,
77
EuiText,
88
EuiCode,
9+
EuiCodeBlock,
910
} from '../../../../src/components';
1011

1112
import { Link } from 'react-router-dom';
@@ -16,6 +17,9 @@ const markdownFormatSource = require('!!raw-loader!./markdown_format');
1617
import MarkdownFormatStyles from './markdown_format_styles';
1718
const markdownFormatStylesSource = require('!!raw-loader!./markdown_format_styles');
1819

20+
import MarkdownFormatLinks, { markdownContent } from './markdown_format_links';
21+
const markdownFormatLinksSource = require('!!raw-loader!./markdown_format_links');
22+
1923
import MarkdownFormatSink from './markdown_format_sink';
2024
const markdownFormatSinkSource = require('!!raw-loader!./markdown_format_sink');
2125

@@ -87,6 +91,31 @@ export const MarkdownFormatExample = {
8791
},
8892
demo: <MarkdownFormatStyles />,
8993
},
94+
{
95+
source: [
96+
{
97+
type: GuideSectionTypes.JS,
98+
code: markdownFormatLinksSource,
99+
},
100+
],
101+
title: 'Link validation for security',
102+
text: (
103+
<>
104+
<p>
105+
Markdown content often comes from untrusted sources like user
106+
generated content. To help with potential security issues,{' '}
107+
<EuiCode>EuiMarkdownRenderer</EuiCode> only renders links if they
108+
begin with <EuiCode>https:</EuiCode>, <EuiCode>http:</EuiCode>,{' '}
109+
<EuiCode>mailto:</EuiCode>, or <EuiCode>/</EuiCode>.
110+
</p>
111+
<EuiCodeBlock language="markdown">{markdownContent}</EuiCodeBlock>
112+
</>
113+
),
114+
props: {
115+
EuiMarkdownFormat,
116+
},
117+
demo: <MarkdownFormatLinks />,
118+
},
90119
{
91120
source: [
92121
{
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
3+
import { EuiMarkdownFormat } from '../../../../src';
4+
5+
// eslint-disable-next-line no-restricted-globals
6+
const locationPathname = location.pathname;
7+
8+
export const markdownContent = `**Links starting with http:, https:, mailto:, and / are valid:**
9+
10+
* https://elastic.com
11+
* http://elastic.com
12+
* https link to [elastic.co](https://elastic.co)
13+
* http link to [elastic.co](http://elastic.co)
14+
* relative link to [eui doc's homepage](${locationPathname})
15+
16+
* [email me!](mailto:[email protected])
17+
18+
**Other link protocols are kept as their markdown source:**
19+
* ftp://elastic.co
20+
* An [ftp link](ftp://elastic.co)
21+
`;
22+
23+
export default () => {
24+
return <EuiMarkdownFormat>{markdownContent}</EuiMarkdownFormat>;
25+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import {
4+
getDefaultEuiMarkdownParsingPlugins,
5+
euiMarkdownLinkValidator,
6+
EuiMarkdownFormat,
7+
} from '../../../../src/components';
8+
9+
// find the validation plugin and configure it to only allow https: and mailto: links
10+
const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
11+
parsingPlugins.find(([plugin, config]) => {
12+
const isValidationPlugin = plugin === euiMarkdownLinkValidator;
13+
if (isValidationPlugin) {
14+
config.allowProtocols = ['https:', 'mailto:'];
15+
}
16+
return isValidationPlugin;
17+
});
18+
19+
const markdown = `**Standalone links**
20+
https://example.com
21+
http://example.com
22+
23+
24+
**As markdown syntax**
25+
[example.com, https](https://example.com)
26+
[example.com, http](http://example.com)
27+
28+
`;
29+
30+
export default () => (
31+
<EuiMarkdownFormat parsingPluginList={parsingPlugins}>
32+
{markdown}
33+
</EuiMarkdownFormat>
34+
);

src-docs/src/views/markdown_editor/markdown_plugin_example.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { Link } from 'react-router-dom';
1919
import MarkdownEditorWithPlugins from './markdown_editor_with_plugins';
2020
const markdownEditorWithPluginsSource = require('!!raw-loader!./markdown_editor_with_plugins');
2121

22+
const linkValidationSource = require('!!raw-loader!./markdown_link_validation');
23+
import LinkValidation from './markdown_link_validation';
24+
2225
const pluginSnippet = `<EuiMarkdownEditor
2326
uiPlugin={myPluginUI}
2427
parsingPluginList={myPluginParsingList}
@@ -170,6 +173,186 @@ export const MarkdownPluginExample = {
170173
),
171174

172175
sections: [
176+
{
177+
title: 'Default plugins',
178+
text: (
179+
<>
180+
<p>
181+
EUI provides additional plugins by default, but these can be omitted
182+
or otherwise customized by providing the{' '}
183+
<EuiCode>parsingPluginList</EuiCode>,{' '}
184+
<EuiCode>processingPluginList</EuiCode>, and{' '}
185+
<EuiCode>uiPlugins</EuiCode> props to the editor and formatter
186+
components.
187+
</p>
188+
<p>The parsing plugins, responsible for parsing markdown are:</p>
189+
<ol>
190+
<li>
191+
<EuiLink
192+
href="https://www.npmjs.com/package/remark-parse"
193+
external
194+
>
195+
remark-parse
196+
</EuiLink>
197+
</li>
198+
<li>
199+
<EuiLink
200+
href="https://github.com/elastic/eui/blob/main/src/components/markdown_editor/plugins/remark/remark_prismjs.ts"
201+
external
202+
>
203+
additional pre-processing for code blocks
204+
</EuiLink>
205+
</li>
206+
<li>
207+
<EuiLink
208+
href="https://www.npmjs.com/package/remark-emoji"
209+
external
210+
>
211+
remark-emoji
212+
</EuiLink>
213+
</li>
214+
<li>
215+
<EuiLink
216+
href="https://www.npmjs.com/package/remark-breaks"
217+
external
218+
>
219+
remark-breaks
220+
</EuiLink>
221+
</li>
222+
<li>
223+
<EuiLink
224+
href="https://github.com/elastic/eui/blob/main/src/components/markdown_editor/plugins/markdown_link_validator.tsx"
225+
external
226+
>
227+
link validation for security
228+
</EuiLink>
229+
</li>
230+
<li>
231+
<EuiLink
232+
href="https://github.com/elastic/eui/blob/main/src/components/markdown_editor/plugins/markdown_checkbox/parser.ts"
233+
external
234+
>
235+
injection of EuiCheckbox for markdown check boxes
236+
</EuiLink>
237+
</li>
238+
<li>
239+
<EuiLink
240+
href="https://github.com/elastic/eui/blob/main/src/components/markdown_editor/plugins/markdown_tooltip/parser.ts"
241+
external
242+
>
243+
tooltip plugin parser
244+
</EuiLink>
245+
</li>
246+
</ol>
247+
<p>
248+
The above set provides an abstract syntax tree used by the editor to
249+
provide feedback, and the renderer passes that output to the set of
250+
processing plugins to allow it to be rendered:
251+
</p>
252+
<ol>
253+
<li>
254+
<EuiLink
255+
href="https://www.npmjs.com/package/remark-rehype"
256+
external
257+
>
258+
remark-rehype
259+
</EuiLink>
260+
</li>
261+
<li>
262+
<EuiLink
263+
href="https://www.npmjs.com/package/rehype-react"
264+
external
265+
>
266+
rehype-react
267+
</EuiLink>
268+
</li>
269+
<li>
270+
<EuiLink
271+
href="https://github.com/elastic/eui/blob/main/src/components/markdown_editor/plugins/markdown_tooltip/renderer.tsx"
272+
external
273+
>
274+
tooltip plugin renderer
275+
</EuiLink>
276+
</li>
277+
</ol>
278+
<p>
279+
The last set of plugin configuration - <EuiCode>uiPlugins</EuiCode>{' '}
280+
- allows toolbar buttons to be defined and how they alter or inject
281+
markdown and returns with only one plugin:
282+
</p>
283+
<ol>
284+
<li>
285+
<EuiLink
286+
href="https://github.com/elastic/eui/blob/main/src/components/markdown_editor/plugins/markdown_tooltip/plugin.tsx"
287+
external
288+
>
289+
tooltip plugin ui
290+
</EuiLink>
291+
</li>
292+
</ol>
293+
<p>
294+
These plugin definitions can be obtained by calling{' '}
295+
<EuiCode>getDefaultEuiMarkdownParsingPlugins</EuiCode>,{' '}
296+
<EuiCode>getDefaultEuiMarkdownProcessingPlugins</EuiCode>, and{' '}
297+
<EuiCode>getDefaultEuiMarkdownUiPlugins</EuiCode> respectively. Each
298+
of these three functions take an optional configuration object with
299+
an <EuiCode>exclude</EuiCode> key, an array of EUI-defaulted plugins
300+
to disable. Currently the only option this configuration can take is{' '}
301+
<EuiCode>&apos;tooltip&apos;</EuiCode>.
302+
</p>
303+
</>
304+
),
305+
},
306+
{
307+
source: [
308+
{
309+
type: GuideSectionTypes.JS,
310+
code: linkValidationSource,
311+
},
312+
],
313+
title: 'Link validation & security',
314+
text: (
315+
<Fragment>
316+
<p>
317+
To enhance user and application security, the default behavior
318+
removes links to URLs that aren&apos;t relative (beginning with{' '}
319+
<EuiCode>/</EuiCode>) and don&apos;t use the{' '}
320+
<EuiCode>https:</EuiCode>, <EuiCode>http:</EuiCode>, or{' '}
321+
<EuiCode>mailto:</EuiCode> protocols. This validation can be further
322+
configured or removed altogether.
323+
</p>
324+
<p>
325+
In this example only <EuiCode>https:</EuiCode> and{' '}
326+
<EuiCode>mailto:</EuiCode> links are allowed.
327+
</p>
328+
</Fragment>
329+
),
330+
snippet: [
331+
`// change what link protocols are allowed
332+
const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
333+
parsingPlugins.find(([plugin, config]) => {
334+
const isValidationPlugin = plugin === euiMarkdownLinkValidator;
335+
if (isValidationPlugin) {
336+
config.allowProtocols = ['https:', 'mailto:'];
337+
}
338+
return isValidationPlugin;
339+
});`,
340+
`// filter out the link validation plugin
341+
const parsingPlugins = getDefaultEuiMarkdownParsingPlugins().filter(([plugin]) => {
342+
return plugin !== euiMarkdownLinkValidator;
343+
});`,
344+
`// disable relative urls
345+
const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
346+
parsingPlugins.find(([plugin, config]) => {
347+
const isValidationPlugin = plugin === euiMarkdownLinkValidator;
348+
if (isValidationPlugin) {
349+
config.allowRelative = false;
350+
}
351+
return isValidationPlugin;
352+
});`,
353+
],
354+
demo: <LinkValidation />,
355+
},
173356
{
174357
wrapText: false,
175358
text: (

src/components/markdown_editor/__snapshots__/markdown_editor.test.tsx.snap

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ exports[`EuiMarkdownEditor custom plugins are excluded and popover is rendered 1
2929
],
3030
Array [
3131
[Function],
32-
Object {},
32+
Object {
33+
"allowProtocols": Array [
34+
"https:",
35+
"http:",
36+
"mailto:",
37+
],
38+
"allowRelative": true,
39+
},
3340
],
3441
Array [
3542
[Function],

src/components/markdown_editor/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ export type {
2626
RemarkRehypeHandler,
2727
RemarkTokenizer,
2828
} from './markdown_types';
29+
export { euiMarkdownLinkValidator } from './plugins/markdown_link_validator';
30+
export type { EuiMarkdownLinkValidatorOptions } from './plugins/markdown_link_validator';

src/components/markdown_editor/plugins/markdown_default_plugins/parsing_plugins.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ import breaks from 'remark-breaks';
2828
import highlight from '../remark/remark_prismjs';
2929
import * as MarkdownTooltip from '../markdown_tooltip';
3030
import * as MarkdownCheckbox from '../markdown_checkbox';
31-
import { markdownLinkValidator } from '../markdown_link_validator';
31+
import {
32+
euiMarkdownLinkValidator,
33+
EuiMarkdownLinkValidatorOptions,
34+
} from '../markdown_link_validator';
3235

3336
export type DefaultEuiMarkdownParsingPlugins = PluggableList;
3437

@@ -41,7 +44,13 @@ export const getDefaultEuiMarkdownParsingPlugins = ({
4144
[highlight, {}],
4245
[emoji, { emoticon: false }],
4346
[breaks, {}],
44-
[markdownLinkValidator, {}],
47+
[
48+
euiMarkdownLinkValidator,
49+
{
50+
allowRelative: true,
51+
allowProtocols: ['https:', 'http:', 'mailto:'],
52+
} as EuiMarkdownLinkValidatorOptions,
53+
],
4554
[MarkdownCheckbox.parser, {}],
4655
];
4756

0 commit comments

Comments
 (0)