diff --git a/package.json b/package.json index c4cf2ac1..0bd13fc2 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@types/chai": "^4.0.5", "@types/cypress": "^0.1.2", + "@types/dompurify": "^0.0.31", "@types/enzyme": "^2.8.10", "@types/enzyme-to-json": "^1.5.0", "@types/jest": "^20.0.8", @@ -72,6 +73,7 @@ "dependencies": { "@types/prop-types": "^15.5.2", "decko": "^1.2.0", + "dompurify": "^1.0.2", "eventemitter3": "^2.0.3", "json-pointer": "^0.6.0", "json-schema-ref-parser": "^3.3.1", diff --git a/src/components/Markdown/Markdown.tsx b/src/components/Markdown/Markdown.tsx index ebbe3a5a..cc419636 100644 --- a/src/components/Markdown/Markdown.tsx +++ b/src/components/Markdown/Markdown.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import styled from '../../styled-components'; import { MarkdownRenderer } from '../../services'; +import { ComponentWithOptions } from '../OptionsProvider'; +import * as DOMPurify from 'dompurify'; import { markdownCss } from './styles'; @@ -12,9 +14,10 @@ interface MarkdownProps { className?: string; raw?: boolean; components?: { [name: string]: React.ComponentClass }; + sanitize?: boolean; } -class InternalMarkdown extends React.PureComponent { +class InternalMarkdown extends ComponentWithOptions { constructor(props: MarkdownProps) { super(props); @@ -24,6 +27,14 @@ class InternalMarkdown extends React.PureComponent { } render() { + let sanitize: (string) => string; + + if (this.props.sanitize || this.options.untrustedSpec) { + sanitize = html => DOMPurify.sanitize(html); + } else { + sanitize = html => html; + } + const renderer = new MarkdownRenderer(); const { source, raw, className, components, inline, dense } = this.props; const parts = components @@ -40,7 +51,7 @@ class InternalMarkdown extends React.PureComponent { return ( ); } @@ -50,7 +61,7 @@ class InternalMarkdown extends React.PureComponent { {parts.map( (part, idx) => typeof part === 'string' ? ( -
+
) : ( ), diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 70428ed9..fa730923 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -11,6 +11,7 @@ export interface RedocRawOptions { noAutoAuth?: boolean | string; nativeScrollbars?: boolean | string; pathInMiddlePanel?: boolean | string; + untrustedSpec?: boolean | string; hideLoading?: boolean | string; } @@ -29,6 +30,7 @@ export class RedocNormalizedOptions { noAutoAuth: boolean; nativeScrollbars: boolean; pathInMiddlePanel: boolean; + untrustedSpec: boolean; constructor(raw: RedocRawOptions) { this.theme = { ...(raw.theme || {}), ...defaultTheme }; // todo: merge deep @@ -39,6 +41,7 @@ export class RedocNormalizedOptions { this.noAutoAuth = argValueToBoolean(raw.noAutoAuth); this.nativeScrollbars = argValueToBoolean(raw.nativeScrollbars); this.pathInMiddlePanel = argValueToBoolean(raw.pathInMiddlePanel); + this.untrustedSpec = argValueToBoolean(raw.untrustedSpec); } static normalizeExpandResponses(value: RedocRawOptions['expandResponses']) { diff --git a/yarn.lock b/yarn.lock index c4bb3c2f..ddf87f72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,10 @@ version "0.1.3" resolved "https://registry.yarnpkg.com/@types/cypress/-/cypress-0.1.3.tgz#1783db79f0d1a44b85aae6a473104e2d7987e022" +"@types/dompurify@^0.0.31": + version "0.0.31" + resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-0.0.31.tgz#f152d5a81f2b5625e29f11eb016cd9b301d0d4b4" + "@types/enzyme-to-json@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@types/enzyme-to-json/-/enzyme-to-json-1.5.0.tgz#0b53c4c8479050e76a38ad298fb2672a7241fad3" @@ -1774,6 +1778,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" +dompurify@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-1.0.2.tgz#7cf47d57614324a9723a1ba69143eeb7d3c617ac" + domutils@1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.1.6.tgz#bddc3de099b9a2efacc51c623f28f416ecc57485"