From 41800f9ee51042d14514bbbda2855a9b847e914d Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Tue, 13 Apr 2021 19:07:33 +0200 Subject: [PATCH] Fix Markdown HTML issue (#658) This change enables notebooks to escape HTML (which is a vector for malicious attacks). We import `MarkdownCell` from the `@nteract/stateful-components` sources so that we can point it to the version of `@nteract/markdown` which contains [this fix](https://github.com/nteract/markdown/commit/e19c7cc590a4379fc507f67a7b4228363b9d8631). This is a temporary workaround from upgrading to `@nteract/stateful-components` to `7.0.0` which causes build and runtime issues see #599). --- package-lock.json | 61 ++++--- package.json | 6 +- .../NotebookRenderer/NotebookRenderer.tsx | 3 +- .../NotebookRenderer/markdown-cell.tsx | 153 ++++++++++++++++++ 4 files changed, 196 insertions(+), 27 deletions(-) create mode 100644 src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx diff --git a/package-lock.json b/package-lock.json index d9a600faa..642bf2a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2829,6 +2829,16 @@ "url": "^0.11.0" }, "dependencies": { + "@types/react": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -2838,6 +2848,11 @@ "ieee754": "^1.1.13" } }, + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + }, "react": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", @@ -3604,19 +3619,19 @@ "integrity": "sha512-6f675p3gzs7ZMAovzfOx+QOMNu1TGVT2aV5lPOwnPxJCM/APLpDRFcSoURwLA26CROlTTDEe10XweFzJgQ+VEQ==" }, "@nteract/markdown": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-4.4.0.tgz", - "integrity": "sha512-Xd8sxPmW42HW2Nq0pz2XrFBARt4wmgA0IbLQ23pg7FRMzpt2Ed4EjfuJkcm9ylTreAt1NJcljIpN47vzBUIehQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-4.6.0.tgz", + "integrity": "sha512-DIeUYSRsFlHlIJ+bz/w1ln/KKtwqr9LsYZ+Uj/2t7mlmYxeEW0JRBa/E51QqCVdEepjAlpg2XqfqNgjkZiFfvw==", "requires": { - "@nteract/mathjax": "^4.0.7", + "@nteract/mathjax": "^4.0.11", "@nteract/presentational-components": "^3.3.11", "react-markdown": "^4.0.0" }, "dependencies": { "@nteract/presentational-components": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.8.tgz", - "integrity": "sha512-gS0Gbxs/Z3mB9TCgz1CU5zBHChhOf3RhkLHsesNf/ljm7rRadzaaYa1NxcgugtxkcnVqt32angl9KfoCYb8R9A==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/@nteract/presentational-components/-/presentational-components-3.4.9.tgz", + "integrity": "sha512-fcCYOdBRFyuj9vvXnrr2L2ynqouHnexUxpzt5VGTs4Mf/72r93vksarBStw2BD19utCVci7Fb5z6tNkFgveAZA==", "requires": { "@blueprintjs/core": "^3.7.0", "@blueprintjs/select": "^3.2.0", @@ -5339,8 +5354,7 @@ "@types/prop-types": { "version": "15.5.8", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.8.tgz", - "integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==", - "dev": true + "integrity": "sha512-3AQoUxQcQtLHsK25wtTWIoIpgYjH3vSDroZOUr7PpCHw/jLY1RB9z9E8dBT/OSmwStVgkRNvdh+ZHNiomRieaw==" }, "@types/puppeteer": { "version": "5.4.3", @@ -5358,30 +5372,26 @@ "dev": true }, "@types/react": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", - "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz", + "integrity": "sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==", "requires": { "@types/prop-types": "*", + "@types/scheduler": "*", "csstype": "^3.0.2" }, "dependencies": { - "@types/prop-types": { - "version": "15.7.3", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" - }, "csstype": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", - "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" } } }, "@types/react-dom": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.0.tgz", - "integrity": "sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz", + "integrity": "sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w==", "dev": true, "requires": { "@types/react": "*" @@ -5430,6 +5440,11 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "@types/scheduler": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz", + "integrity": "sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==" + }, "@types/shallowequal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz", diff --git a/package.json b/package.json index cc1f0ea53..4dd2e1663 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "@nteract/iron-icons": "1.0.0", "@nteract/jupyter-widgets": "2.0.0", "@nteract/logos": "1.0.0", - "@nteract/markdown": "4.4.0", + "@nteract/markdown": "4.6.0", "@nteract/monaco-editor": "3.2.2", "@nteract/octicons": "2.0.0", "@nteract/outputs": "3.0.9", @@ -120,8 +120,8 @@ "@types/prop-types": "15.5.8", "@types/puppeteer": "5.4.3", "@types/q": "1.5.1", - "@types/react": "17.0.0", - "@types/react-dom": "17.0.0", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.3", "@types/react-notification-system": "0.2.39", "@types/react-redux": "7.1.7", "@types/sinon": "2.3.3", diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx index eea9ac076..b78fdab97 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx @@ -2,7 +2,7 @@ import { CellId } from "@nteract/commutable"; import { CellType } from "@nteract/commutable/src"; import { actions, ContentRef } from "@nteract/core"; import { KernelOutputError, StreamText } from "@nteract/outputs"; -import { Cells, CodeCell, MarkdownCell, RawCell } from "@nteract/stateful-components"; +import { Cells, CodeCell, RawCell } from "@nteract/stateful-components"; import MonacoEditor from "@nteract/stateful-components/lib/inputs/connected-editors/monacoEditor"; import { PassedEditorProps } from "@nteract/stateful-components/lib/inputs/editor"; import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media"; @@ -21,6 +21,7 @@ import CellLabeler from "./decorators/CellLabeler"; import HoverableCell from "./decorators/HoverableCell"; import KeyboardShortcuts from "./decorators/kbd-shortcuts"; import "./default.css"; +import MarkdownCell from "./markdown-cell"; import "./NotebookRenderer.less"; import IFrameOutputs from "./outputs/IFrameOutputs"; import Prompt from "./Prompt"; diff --git a/src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx b/src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx new file mode 100644 index 000000000..1d6e898a7 --- /dev/null +++ b/src/Explorer/Notebook/NotebookRenderer/markdown-cell.tsx @@ -0,0 +1,153 @@ +// TODO The purpose of importing this source file https://github.com/nteract/nteract/blob/main/packages/stateful-components/src/cells/markdown-cell.tsx +// into our source is to be able to overwrite the version of react-markdown which has this fix ("escape html to false") +// https://github.com/nteract/markdown/commit/e19c7cc590a4379fc507f67a7b4228363b9d8631 without having to upgrade +// @nteract/stateful-component which causes runtime issues. + +import { ImmutableCell } from "@nteract/commutable/src"; +import { actions, AppState, ContentRef, selectors } from "@nteract/core"; +import { MarkdownPreviewer } from "@nteract/markdown"; +import { defineConfigOption } from "@nteract/mythic-configuration"; +import { Source } from "@nteract/presentational-components"; +import Editor, { EditorSlots } from "@nteract/stateful-components/lib/inputs/editor"; +import React from "react"; +import { ReactMarkdownProps } from "react-markdown"; +import { connect } from "react-redux"; +import { Dispatch } from "redux"; + +const { selector: markdownConfig } = defineConfigOption({ + key: "markdownOptions", + label: "Markdown Editor Options", + defaultValue: {}, +}); + +interface NamedMDCellSlots { + editor?: EditorSlots; + toolbar?: () => JSX.Element; +} + +interface ComponentProps { + id: string; + contentRef: ContentRef; + cell_type?: "markdown"; + children?: NamedMDCellSlots; +} + +interface StateProps { + isCellFocused: boolean; + isEditorFocused: boolean; + cell?: ImmutableCell; + markdownOptions: ReactMarkdownProps; +} + +interface DispatchProps { + focusAboveCell: () => void; + focusBelowCell: () => void; + focusEditor: () => void; + unfocusEditor: () => void; +} + +export class PureMarkdownCell extends React.Component { + render() { + const { contentRef, id, cell, children } = this.props; + + const { isEditorFocused, isCellFocused, markdownOptions } = this.props; + + const { focusAboveCell, focusBelowCell, focusEditor, unfocusEditor } = this.props; + + /** + * We don't set the editor slots as defaults to support dynamic imports + * Users can continue to add the editorSlots as children + */ + const editor = children?.editor; + const toolbar = children?.toolbar; + + const source = cell ? cell.get("source", "") : ""; + + return ( +
+
+
{toolbar && toolbar()}
+
+ + + + {editor} + + + +
+
+
+ ); + } +} + +export const makeMapStateToProps = ( + initialState: AppState, + ownProps: ComponentProps +): ((state: AppState) => StateProps) => { + const { id, contentRef } = ownProps; + const mapStateToProps = (state: AppState): StateProps => { + const model = selectors.model(state, { contentRef }); + let isCellFocused = false; + let isEditorFocused = false; + let cell; + + if (model && model.type === "notebook") { + cell = selectors.notebook.cellById(model, { id }); + isCellFocused = model.cellFocused === id; + isEditorFocused = model.editorFocused === id; + } + + const markdownOptionsDefaults = { + linkTarget: "_blank", + }; + const currentMarkdownOptions = markdownConfig(state); + + const markdownOptions = Object.assign({}, markdownOptionsDefaults, currentMarkdownOptions); + + return { + cell, + isCellFocused, + isEditorFocused, + markdownOptions, + }; + }; + + return mapStateToProps; +}; + +const makeMapDispatchToProps = ( + initialDispatch: Dispatch, + ownProps: ComponentProps +): ((dispatch: Dispatch) => DispatchProps) => { + const { id, contentRef } = ownProps; + + const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + focusAboveCell: () => { + dispatch(actions.focusPreviousCell({ id, contentRef })); + dispatch(actions.focusPreviousCellEditor({ id, contentRef })); + }, + focusBelowCell: () => { + dispatch(actions.focusNextCell({ id, createCellIfUndefined: true, contentRef })); + dispatch(actions.focusNextCellEditor({ id, contentRef })); + }, + focusEditor: () => dispatch(actions.focusCellEditor({ id, contentRef })), + unfocusEditor: () => dispatch(actions.focusCellEditor({ id: undefined, contentRef })), + }); + + return mapDispatchToProps; +}; + +const MarkdownCell = connect(makeMapStateToProps, makeMapDispatchToProps)(PureMarkdownCell); + +export default MarkdownCell;