refactor(ui): use rehype-sanitize for gfm sanitization (#31770)
Signed-off-by: Zach Callahan <zach@zmc.dev>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
The MarkdownContent component now handles HTML content the same way as GitHub when rendering GitHub-flavored Markdown
|
||||
@@ -77,6 +77,7 @@
|
||||
"linkify-react": "4.3.2",
|
||||
"linkifyjs": "4.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"parse5": "^6.0.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"qs": "^6.9.4",
|
||||
"rc-progress": "3.5.1",
|
||||
@@ -90,6 +91,8 @@
|
||||
"react-use": "^17.3.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.11",
|
||||
"react-window": "^1.8.6",
|
||||
"rehype-raw": "^6.0.0",
|
||||
"rehype-sanitize": "^5.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "^3.22.4"
|
||||
|
||||
@@ -45,6 +45,29 @@ const markdownGithubFlavored =
|
||||
'* [ ] to do\n' +
|
||||
'* [x] done';
|
||||
|
||||
const markdownGithubFlavoredWithHTML =
|
||||
'# GFM with HTML\n' +
|
||||
'\n' +
|
||||
'This is a paragraph with <strong>bold text</strong> and <em>italic text</em>.\n' +
|
||||
'\n' +
|
||||
'<br />\n' +
|
||||
'\n' +
|
||||
'Here is a list:\n' +
|
||||
'\n' +
|
||||
'<ul>\n' +
|
||||
' <li>First item</li>\n' +
|
||||
' <li>Second item with <a href="https://example.com">a link</a></li>\n' +
|
||||
' <li>Third item</li>\n' +
|
||||
'</ul>\n' +
|
||||
'\n' +
|
||||
'And a code block:\n' +
|
||||
'\n' +
|
||||
'<pre><code class="language-js">\n' +
|
||||
'function greet() {\n' +
|
||||
' console.log("Hello, world!");\n' +
|
||||
'}\n' +
|
||||
'</code></pre>\n';
|
||||
|
||||
const markdown =
|
||||
'# Choreas Iovis\n' +
|
||||
'\n' +
|
||||
@@ -117,3 +140,7 @@ export const MarkdownContentCommonMark = () => (
|
||||
export const MarkdownContentGithubFlavoredCommonMark = () => (
|
||||
<MarkdownContent content={markdownGithubFlavored} dialect="gfm" />
|
||||
);
|
||||
|
||||
export const MarkdownContentGithubFlavoredWithHTML = () => (
|
||||
<MarkdownContent content={markdownGithubFlavoredWithHTML} dialect="gfm" />
|
||||
);
|
||||
|
||||
@@ -163,4 +163,52 @@ describe('<MarkdownContent />', () => {
|
||||
'the-fitnessgram-pacer-test-is-a-multistage-aerobic-capacity-test',
|
||||
);
|
||||
});
|
||||
|
||||
it('render MarkdownContent component with br tags for new lines in GFM dialect', async () => {
|
||||
await renderInTestApp(
|
||||
<MarkdownContent
|
||||
content="<p>Line 1</p><br /><p>Line 2</p><br><p>Line 3</p><br />"
|
||||
dialect="gfm"
|
||||
/>,
|
||||
);
|
||||
|
||||
const line1 = screen.getByText(/Line 1/);
|
||||
const line2 = screen.getByText(/Line 2/);
|
||||
const line3 = screen.getByText(/Line 3/);
|
||||
|
||||
expect(line1.nextSibling?.nodeName).toBe('BR');
|
||||
expect(line2.previousSibling?.nodeName).toBe('BR');
|
||||
expect(line2.nextSibling?.nodeName).toBe('BR');
|
||||
expect(line3.previousSibling?.nodeName).toBe('BR');
|
||||
});
|
||||
|
||||
it('render MarkdownContent component without allowing inline styles in GFM dialect', async () => {
|
||||
await renderInTestApp(
|
||||
<MarkdownContent
|
||||
content='<div style="color: blue; border: 1px solid black; padding: 10px;">This is a custom HTML block with inline styles.</div>'
|
||||
dialect="gfm"
|
||||
/>,
|
||||
);
|
||||
|
||||
const divElement = screen.getByText(
|
||||
'This is a custom HTML block with inline styles.',
|
||||
);
|
||||
expect(divElement).toBeInTheDocument();
|
||||
expect(divElement).not.toHaveStyle('color: blue');
|
||||
expect(divElement).not.toHaveStyle('border: 1px solid black');
|
||||
expect(divElement).not.toHaveStyle('padding: 10px');
|
||||
});
|
||||
|
||||
it('render MarkdownContent component without disallowed elements in GFM dialect', async () => {
|
||||
const { container } = await renderInTestApp(
|
||||
<MarkdownContent
|
||||
content='<script>alert("XSS Attack!");</script><style>body { background-color: red; }</style><p>Safe Content</p>'
|
||||
dialect="gfm"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Safe Content')).toBeInTheDocument();
|
||||
expect(container.querySelector('script')).toBeNull();
|
||||
expect(container.querySelector('style')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,9 @@ import gfm from 'remark-gfm';
|
||||
import { Children, createElement } from 'react';
|
||||
import { CodeSnippet } from '../CodeSnippet';
|
||||
import { HeadingProps } from 'react-markdown/lib/ast-to-react';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
|
||||
import type { PluggableList } from 'react-markdown/lib/react-markdown';
|
||||
|
||||
export type MarkdownContentClassKey = 'markdown';
|
||||
|
||||
@@ -108,6 +111,30 @@ const components: Options['components'] = {
|
||||
h6: headingRenderer,
|
||||
};
|
||||
|
||||
const gfmRehypePlugins: PluggableList = [
|
||||
[
|
||||
rehypeRaw,
|
||||
{
|
||||
tagFiter: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
rehypeSanitize,
|
||||
{
|
||||
...defaultSchema,
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
code: [
|
||||
...(defaultSchema.attributes?.code ?? []),
|
||||
// for syntax highlighting classes in code blocks
|
||||
// breaks the codesnippet component override above if omitted
|
||||
['className'],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Renders markdown with the default dialect {@link https://github.github.com/gfm/ | gfm - GitHub flavored Markdown} to backstage theme styled HTML.
|
||||
*
|
||||
@@ -127,6 +154,7 @@ export function MarkdownContent(props: Props) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={dialect === 'gfm' ? [gfm] : []}
|
||||
rehypePlugins={dialect === 'gfm' ? gfmRehypePlugins : []}
|
||||
className={`${classes.markdown} ${className ?? ''}`.trim()}
|
||||
children={content}
|
||||
components={components}
|
||||
|
||||
@@ -3645,6 +3645,7 @@ __metadata:
|
||||
linkifyjs: "npm:4.3.2"
|
||||
lodash: "npm:^4.17.21"
|
||||
msw: "npm:^1.0.0"
|
||||
parse5: "npm:^6.0.0"
|
||||
pluralize: "npm:^8.0.0"
|
||||
qs: "npm:^6.9.4"
|
||||
rc-progress: "npm:3.5.1"
|
||||
@@ -3661,6 +3662,8 @@ __metadata:
|
||||
react-use: "npm:^17.3.2"
|
||||
react-virtualized-auto-sizer: "npm:^1.0.11"
|
||||
react-window: "npm:^1.8.6"
|
||||
rehype-raw: "npm:^6.0.0"
|
||||
rehype-sanitize: "npm:^5.0.0"
|
||||
remark-gfm: "npm:^3.0.1"
|
||||
zen-observable: "npm:^0.10.0"
|
||||
zod: "npm:^3.22.4"
|
||||
@@ -21464,6 +21467,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/parse5@npm:^6.0.0":
|
||||
version: 6.0.3
|
||||
resolution: "@types/parse5@npm:6.0.3"
|
||||
checksum: 10/834d40c9b1a8a99a9574b0b3f6629cf48adcff2eda01a35d701f1de5dcf46ce24223684647890aba9f985d6c801b233f878168683de0ae425940403c383fba8f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/passport-auth0@npm:^1.0.5":
|
||||
version: 1.0.9
|
||||
resolution: "@types/passport-auth0@npm:1.0.9"
|
||||
@@ -33057,6 +33067,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-from-parse5@npm:^7.0.0":
|
||||
version: 7.1.2
|
||||
resolution: "hast-util-from-parse5@npm:7.1.2"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
"@types/unist": "npm:^2.0.0"
|
||||
hastscript: "npm:^7.0.0"
|
||||
property-information: "npm:^6.0.0"
|
||||
vfile: "npm:^5.0.0"
|
||||
vfile-location: "npm:^4.0.0"
|
||||
web-namespaces: "npm:^2.0.0"
|
||||
checksum: 10/7a90a16430a1482ed1be5c2f8b182e8b12aee8834781245b101700b5a04cea8b569cf40ef08214e1eb333249432e861b17e6fe46d0447b5281827c8798e86f1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-parse-selector@npm:^2.0.0":
|
||||
version: 2.2.4
|
||||
resolution: "hast-util-parse-selector@npm:2.2.4"
|
||||
@@ -33064,6 +33089,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-parse-selector@npm:^3.0.0":
|
||||
version: 3.1.1
|
||||
resolution: "hast-util-parse-selector@npm:3.1.1"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
checksum: 10/511d373465f60dd65e924f88bf0954085f4fb6e3a2b062a4b5ac43b93cbfd36a8dce6234b5d1e3e63499d936375687e83fc5da55628b22bd6b581b5ee167d1c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-parse-selector@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "hast-util-parse-selector@npm:4.0.0"
|
||||
@@ -33073,6 +33107,48 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-raw@npm:^7.2.0":
|
||||
version: 7.2.3
|
||||
resolution: "hast-util-raw@npm:7.2.3"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
"@types/parse5": "npm:^6.0.0"
|
||||
hast-util-from-parse5: "npm:^7.0.0"
|
||||
hast-util-to-parse5: "npm:^7.0.0"
|
||||
html-void-elements: "npm:^2.0.0"
|
||||
parse5: "npm:^6.0.0"
|
||||
unist-util-position: "npm:^4.0.0"
|
||||
unist-util-visit: "npm:^4.0.0"
|
||||
vfile: "npm:^5.0.0"
|
||||
web-namespaces: "npm:^2.0.0"
|
||||
zwitch: "npm:^2.0.0"
|
||||
checksum: 10/fe39d4b9e68de7131ec61e9abe887cc0579dc7491f738735150c6021565fc998e37c9d096e97fc35c769e986e04b721d4724835ee82fcc22076d778acf6c4832
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-sanitize@npm:^4.0.0":
|
||||
version: 4.1.0
|
||||
resolution: "hast-util-sanitize@npm:4.1.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
checksum: 10/8258ba32be9e57e871f894d3c6b3187dd98e0682bf1382cd1306c20045cd5dc209589919817dc079803d9a259b420d9cd88771535088c9fe0eea122028c348eb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-to-parse5@npm:^7.0.0":
|
||||
version: 7.1.0
|
||||
resolution: "hast-util-to-parse5@npm:7.1.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
comma-separated-tokens: "npm:^2.0.0"
|
||||
property-information: "npm:^6.0.0"
|
||||
space-separated-tokens: "npm:^2.0.0"
|
||||
web-namespaces: "npm:^2.0.0"
|
||||
zwitch: "npm:^2.0.0"
|
||||
checksum: 10/695539881431f9713ca4a0be7d06bf3e57ae4d9f930eccba371534c50cff11855d345f8ec30099d04482637ad82e3c70d480269bfa4c109f37993536e8ea690d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hast-util-whitespace@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "hast-util-whitespace@npm:2.0.0"
|
||||
@@ -33093,6 +33169,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hastscript@npm:^7.0.0":
|
||||
version: 7.2.0
|
||||
resolution: "hastscript@npm:7.2.0"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
comma-separated-tokens: "npm:^2.0.0"
|
||||
hast-util-parse-selector: "npm:^3.0.0"
|
||||
property-information: "npm:^6.0.0"
|
||||
space-separated-tokens: "npm:^2.0.0"
|
||||
checksum: 10/98740e0b69b4765a23d0174fb93eb1c1bdcae6a9f1c9e1b07de6aca2d578427a42e1d45ee98eda26463ac58ff73a8ce45af19c4eb8b5f6f768a9c8543964d28f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hastscript@npm:^9.0.0":
|
||||
version: 9.0.1
|
||||
resolution: "hastscript@npm:9.0.1"
|
||||
@@ -33300,6 +33389,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-void-elements@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "html-void-elements@npm:2.0.1"
|
||||
checksum: 10/06d41f13b9d5d6e0f39861c4bec9a9196fa4906d56cd5cf6cf54ad2e52a85bf960cca2bf9600026bde16c8331db171bedba5e5a35e2e43630c8f1d497b2fb658
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-webpack-plugin@npm:^5.6.3":
|
||||
version: 5.6.5
|
||||
resolution: "html-webpack-plugin@npm:5.6.5"
|
||||
@@ -41217,6 +41313,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse5@npm:^6.0.0":
|
||||
version: 6.0.1
|
||||
resolution: "parse5@npm:6.0.1"
|
||||
checksum: 10/dfb110581f62bd1425725a7c784ae022a24669bd0efc24b58c71fc731c4d868193e2ebd85b74cde2dbb965e4dcf07059b1e651adbec1b3b5267531bd132fdb75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parse5@npm:^7.0.0, parse5@npm:^7.1.2":
|
||||
version: 7.3.0
|
||||
resolution: "parse5@npm:7.3.0"
|
||||
@@ -44471,6 +44574,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rehype-raw@npm:^6.0.0":
|
||||
version: 6.1.1
|
||||
resolution: "rehype-raw@npm:6.1.1"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
hast-util-raw: "npm:^7.2.0"
|
||||
unified: "npm:^10.0.0"
|
||||
checksum: 10/3599d22c45264bea52c93eec2136f50f119282c0bd4e9604aeb2421fe20db84f9c4536caebf64f29158d8c2403b6fd3b3da634211393fdda9cdd500149d00ae4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rehype-sanitize@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "rehype-sanitize@npm:5.0.1"
|
||||
dependencies:
|
||||
"@types/hast": "npm:^2.0.0"
|
||||
hast-util-sanitize: "npm:^4.0.0"
|
||||
unified: "npm:^10.0.0"
|
||||
checksum: 10/b9d9efc0b3c894fe5da84558147148c355c933794c3411dc7c02278b28ea251d241020cd26836e5cc5c979e0e058b45ca51a0eb87824ffdb7241efecd65b6d29
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"relateurl@npm:^0.2.7":
|
||||
version: 0.2.7
|
||||
resolution: "relateurl@npm:0.2.7"
|
||||
@@ -49700,6 +49825,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vfile-location@npm:^4.0.0":
|
||||
version: 4.1.0
|
||||
resolution: "vfile-location@npm:4.1.0"
|
||||
dependencies:
|
||||
"@types/unist": "npm:^2.0.0"
|
||||
vfile: "npm:^5.0.0"
|
||||
checksum: 10/c894e8e5224170d1f85288f4a1d1ebcee0780823ea2b49d881648ab360ebf01b37ecb09b1c4439a75f9a51f31a9f9742cd045e987763e367c352a1ef7c50d446
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vfile-message@npm:^3.0.0":
|
||||
version: 3.0.2
|
||||
resolution: "vfile-message@npm:3.0.2"
|
||||
@@ -49924,6 +50059,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-namespaces@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "web-namespaces@npm:2.0.1"
|
||||
checksum: 10/b6d9f02f1a43d0ef0848a812d89c83801d5bbad57d8bb61f02eb6d7eb794c3736f6cc2e1191664bb26136594c8218ac609f4069722c6f56d9fc2d808fa9271c6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"web-streams-polyfill@npm:4.0.0-beta.3":
|
||||
version: 4.0.0-beta.3
|
||||
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
|
||||
|
||||
Reference in New Issue
Block a user