safe-mdx 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -4
- package/dist/plugins.d.ts +12 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +68 -0
- package/dist/plugins.js.map +1 -0
- package/dist/safe-mdx.d.ts +10 -9
- package/dist/safe-mdx.d.ts.map +1 -1
- package/dist/safe-mdx.js +381 -70
- package/dist/safe-mdx.js.map +1 -1
- package/dist/safe-mdx.test.js +182 -20
- package/dist/safe-mdx.test.js.map +1 -1
- package/package.json +54 -51
- package/src/plugins.ts +87 -0
- package/src/safe-mdx.test.tsx +190 -20
- package/src/safe-mdx.tsx +367 -56
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/src/index.ts +0 -1
package/README.md
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- Render MDX without `eval
|
|
13
|
+
- Render MDX without `eval` on the server, so you can render MDX in Cloudflare Workers and Vercel Edge
|
|
14
14
|
- Works with React Server Components
|
|
15
15
|
- Supports custom MDX components
|
|
16
16
|
|
|
17
17
|
## Why
|
|
18
18
|
|
|
19
|
-
The default MDX renderer uses `eval` (or `new Function(code)`) to render MDX components. This is a security risk if the MdX code comes from untrusted sources and it's not allowed in some environments like Cloudflare Workers.
|
|
19
|
+
The default MDX renderer uses `eval` (or `new Function(code)`) to render MDX components in the server. This is a security risk if the MdX code comes from untrusted sources and it's not allowed in some environments like Cloudflare Workers.
|
|
20
|
+
|
|
21
|
+
For example in an hypothetical platform similar to Notion, where users can write Markdown and publish it as a website, an user could be able to write MDX code that extracts secrets from the server in the SSR pass, using this library that is not possible. This is what happened with Mintlify platform in 2024.
|
|
20
22
|
|
|
21
23
|
Some use cases for this package are:
|
|
22
24
|
|
|
@@ -47,7 +49,7 @@ This is a paragraph
|
|
|
47
49
|
|
|
48
50
|
export function Page() {
|
|
49
51
|
return (
|
|
50
|
-
<
|
|
52
|
+
<SafeMdxRenderer
|
|
51
53
|
code={code}
|
|
52
54
|
components={{
|
|
53
55
|
// You can pass your own components here
|
|
@@ -97,7 +99,43 @@ const parser = remark().use(remarkMdx)
|
|
|
97
99
|
const mdast = parser.parse(code)
|
|
98
100
|
|
|
99
101
|
export function Page() {
|
|
100
|
-
return <
|
|
102
|
+
return <SafeMdxRenderer code={code} mdast={mdast} />
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Reading the frontmatter
|
|
107
|
+
|
|
108
|
+
safe-mdx renderer ignores the frontmatter, to get its values you wil have to parse the MDX to mdast and read it there.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { SafeMdxRenderer } from 'safe-mdx'
|
|
112
|
+
import { remark } from 'remark'
|
|
113
|
+
import remarkFrontmatter from 'remark-frontmatter'
|
|
114
|
+
import { Yaml } from 'mdast'
|
|
115
|
+
import yaml from 'js-yaml'
|
|
116
|
+
import remarkMdx from 'remark-mdx'
|
|
117
|
+
|
|
118
|
+
const code = `
|
|
119
|
+
---
|
|
120
|
+
hello: 5
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# Hello world
|
|
124
|
+
`
|
|
125
|
+
|
|
126
|
+
export function Page() {
|
|
127
|
+
const parser = remark().use(remarkFrontmatter, ['yaml']).use(remarkMdx)
|
|
128
|
+
|
|
129
|
+
const mdast = parser.parse(code)
|
|
130
|
+
|
|
131
|
+
const yamlFrontmatter = mdast.children.find(
|
|
132
|
+
(node) => node.type === 'yaml',
|
|
133
|
+
) as Yaml
|
|
134
|
+
|
|
135
|
+
const parsedFrontmatter = yaml.load(yamlFrontmatter.value || '')
|
|
136
|
+
|
|
137
|
+
console.log(parsedFrontmatter)
|
|
138
|
+
return <SafeMdxRenderer code={code} mdast={mdast} />
|
|
101
139
|
}
|
|
102
140
|
```
|
|
103
141
|
|
|
@@ -120,6 +158,14 @@ export function Page() {
|
|
|
120
158
|
}
|
|
121
159
|
```
|
|
122
160
|
|
|
161
|
+
## Security
|
|
162
|
+
|
|
163
|
+
safe-mdx is designed to avoid server-side evaluation of untrusted MDX input.
|
|
164
|
+
|
|
165
|
+
However, it's important to note that safe-mdx does not provide protection against client-side vulnerabilities, such as Cross-Site Scripting (XSS) or script injection attacks. While safe-mdx itself does not perform any evaluation or rendering of user-provided content, the rendering library or components used in conjunction with safe-mdx may introduce security risks if not properly configured or sanitized.
|
|
166
|
+
|
|
167
|
+
This is ok if you render your MDX in isolation from each tenant, for example on different subdomains, this way an XSS attack cannot affect all tenants. If instead you render the MDX from different tenants on the same domain, one tenant could steal cookies set from other customers.
|
|
168
|
+
|
|
123
169
|
## Limitations
|
|
124
170
|
|
|
125
171
|
These features are not supported yet:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Root } from 'mdast';
|
|
2
|
+
/**
|
|
3
|
+
* https://github.com/mdx-js/mdx/blob/b3351fadcb6f78833a72757b7135dcfb8ab646fe/packages/mdx/lib/plugin/remark-mark-and-unravel.js
|
|
4
|
+
* A tiny plugin that unravels `<p><h1>x</h1></p>` but also
|
|
5
|
+
* `<p><Component /></p>` (so it has no knowledge of "HTML").
|
|
6
|
+
*
|
|
7
|
+
* It also marks JSX as being explicitly JSX, so when a user passes a `h1`
|
|
8
|
+
* component, it is used for `# heading` but not for `<h1>heading</h1>`.
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
export declare function remarkMarkAndUnravel(): (tree: Root) => void;
|
|
12
|
+
//# sourceMappingURL=plugins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAe,MAAM,OAAO,CAAA;AAKzC;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,WACT,IAAI,UAuE9B"}
|
package/dist/plugins.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { collapseWhiteSpace } from 'collapse-white-space';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
/**
|
|
4
|
+
* https://github.com/mdx-js/mdx/blob/b3351fadcb6f78833a72757b7135dcfb8ab646fe/packages/mdx/lib/plugin/remark-mark-and-unravel.js
|
|
5
|
+
* A tiny plugin that unravels `<p><h1>x</h1></p>` but also
|
|
6
|
+
* `<p><Component /></p>` (so it has no knowledge of "HTML").
|
|
7
|
+
*
|
|
8
|
+
* It also marks JSX as being explicitly JSX, so when a user passes a `h1`
|
|
9
|
+
* component, it is used for `# heading` but not for `<h1>heading</h1>`.
|
|
10
|
+
*
|
|
11
|
+
*/
|
|
12
|
+
export function remarkMarkAndUnravel() {
|
|
13
|
+
return function (tree) {
|
|
14
|
+
visit(tree, function (node, index, parent) {
|
|
15
|
+
let offset = -1;
|
|
16
|
+
let all = true;
|
|
17
|
+
let oneOrMore = false;
|
|
18
|
+
if (parent &&
|
|
19
|
+
typeof index === 'number' &&
|
|
20
|
+
node.type === 'paragraph') {
|
|
21
|
+
const children = node.children;
|
|
22
|
+
while (++offset < children.length) {
|
|
23
|
+
const child = children[offset];
|
|
24
|
+
if (child.type === 'mdxJsxTextElement' ||
|
|
25
|
+
child.type === 'mdxTextExpression') {
|
|
26
|
+
oneOrMore = true;
|
|
27
|
+
}
|
|
28
|
+
else if (child.type === 'text' &&
|
|
29
|
+
collapseWhiteSpace(child.value, {
|
|
30
|
+
style: 'html',
|
|
31
|
+
trim: true,
|
|
32
|
+
}) === '') {
|
|
33
|
+
// Empty.
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
all = false;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (all && oneOrMore) {
|
|
41
|
+
offset = -1;
|
|
42
|
+
const newChildren = [];
|
|
43
|
+
while (++offset < children.length) {
|
|
44
|
+
const child = children[offset];
|
|
45
|
+
if (child.type === 'mdxJsxTextElement') {
|
|
46
|
+
// @ts-expect-error: mutate because it is faster; content model is fine.
|
|
47
|
+
child.type = 'mdxJsxFlowElement';
|
|
48
|
+
}
|
|
49
|
+
if (child.type === 'mdxTextExpression') {
|
|
50
|
+
// @ts-expect-error: mutate because it is faster; content model is fine.
|
|
51
|
+
child.type = 'mdxFlowExpression';
|
|
52
|
+
}
|
|
53
|
+
if (child.type === 'text' &&
|
|
54
|
+
/^[\t\r\n ]+$/.test(String(child.value))) {
|
|
55
|
+
// Empty.
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
newChildren.push(child);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
parent.children.splice(index, 1, ...newChildren);
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=plugins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AACzD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAGxC;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB;IAChC,OAAO,UAAU,IAAU;QAEvB,KAAK,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,KAAK,EAAE,MAAM;YACrC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAA;YACf,IAAI,GAAG,GAAG,IAAI,CAAA;YACd,IAAI,SAAS,GAAG,KAAK,CAAA;YAGrB,IACI,MAAM;gBACN,OAAO,KAAK,KAAK,QAAQ;gBACzB,IAAI,CAAC,IAAI,KAAK,WAAW,EAC3B;gBACE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAA;gBAE9B,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE;oBAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;oBAE9B,IACI,KAAK,CAAC,IAAI,KAAK,mBAAmB;wBAClC,KAAK,CAAC,IAAI,KAAK,mBAAmB,EACpC;wBACE,SAAS,GAAG,IAAI,CAAA;qBACnB;yBAAM,IACH,KAAK,CAAC,IAAI,KAAK,MAAM;wBACrB,kBAAkB,CAAC,KAAK,CAAC,KAAK,EAAE;4BAC5B,KAAK,EAAE,MAAM;4BACb,IAAI,EAAE,IAAI;yBACb,CAAC,KAAK,EAAE,EACX;wBACE,SAAS;qBACZ;yBAAM;wBACH,GAAG,GAAG,KAAK,CAAA;wBACX,MAAK;qBACR;iBACJ;gBAED,IAAI,GAAG,IAAI,SAAS,EAAE;oBAClB,MAAM,GAAG,CAAC,CAAC,CAAA;oBAEX,MAAM,WAAW,GAAkB,EAAE,CAAA;oBAErC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE;wBAC/B,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;wBAE9B,IAAI,KAAK,CAAC,IAAI,KAAK,mBAAmB,EAAE;4BACpC,wEAAwE;4BACxE,KAAK,CAAC,IAAI,GAAG,mBAAmB,CAAA;yBACnC;wBAED,IAAI,KAAK,CAAC,IAAI,KAAK,mBAAmB,EAAE;4BACpC,wEAAwE;4BACxE,KAAK,CAAC,IAAI,GAAG,mBAAmB,CAAA;yBACnC;wBAED,IACI,KAAK,CAAC,IAAI,KAAK,MAAM;4BACrB,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAC1C;4BACE,SAAS;yBACZ;6BAAM;4BACH,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;yBAC1B;qBACJ;oBAED,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,WAAW,CAAC,CAAA;oBAChD,OAAO,KAAK,CAAA;iBACf;aACJ;QACL,CAAC,CAAC,CAAA;IACN,CAAC,CAAA;AACL,CAAC"}
|
package/dist/safe-mdx.d.ts
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { Node, Parent } from 'mdast';
|
|
2
|
-
import { Root
|
|
1
|
+
import { Node, Parent, RootContent } from 'mdast';
|
|
2
|
+
import { Root } from 'mdast';
|
|
3
3
|
import { MdxJsxFlowElement, MdxJsxTextElement } from 'mdast-util-mdx-jsx';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
5
5
|
type MyRootContent = RootContent | Root;
|
|
6
|
+
export declare function mdxParse(code: string): Root;
|
|
6
7
|
export declare function SafeMdxRenderer({ components, code, mdast, }: {
|
|
7
8
|
components: any;
|
|
8
|
-
code?: string
|
|
9
|
+
code?: string;
|
|
9
10
|
mdast?: any;
|
|
10
11
|
}): any;
|
|
11
|
-
declare const nativeTags: readonly ["blockquote", "strong", "em", "del", "hr", "a", "b", "br", "button", "div", "form", "h1", "h2", "h3", "h4", "head", "iframe", "img", "input", "label", "li", "link", "ol", "p", "path", "picture", "script", "section", "source", "span", "sub", "sup", "svg", "table", "tbody", "td", "th", "thead", "tr", "ul", "video", "code", "pre"];
|
|
12
|
-
type ComponentsMap = {
|
|
13
|
-
[k in (typeof nativeTags)[number]]?: any;
|
|
14
|
-
};
|
|
15
12
|
export declare class MdastToJsx {
|
|
16
13
|
mdast: MyRootContent;
|
|
17
14
|
str: string;
|
|
@@ -21,9 +18,9 @@ export declare class MdastToJsx {
|
|
|
21
18
|
message: string;
|
|
22
19
|
}[];
|
|
23
20
|
constructor({ code, mdast, components, }: {
|
|
24
|
-
code?: string
|
|
21
|
+
code?: string;
|
|
25
22
|
mdast?: any;
|
|
26
|
-
components?: ComponentsMap
|
|
23
|
+
components?: ComponentsMap;
|
|
27
24
|
});
|
|
28
25
|
mapMdastChildren(node: any): any;
|
|
29
26
|
mapJsxChildren(node: any): any;
|
|
@@ -35,5 +32,9 @@ export declare function getJsxAttrs(node: MdxJsxFlowElement | MdxJsxTextElement,
|
|
|
35
32
|
message: string;
|
|
36
33
|
}) => void): [string, any][];
|
|
37
34
|
export declare function mdastBfs(node: Parent | Node, cb?: (node: Node | Parent) => any): any[];
|
|
35
|
+
declare const nativeTags: readonly ["blockquote", "strong", "em", "del", "hr", "a", "b", "br", "button", "div", "form", "h1", "h2", "h3", "h4", "head", "iframe", "img", "input", "label", "li", "link", "ol", "p", "path", "picture", "script", "section", "source", "span", "sub", "sup", "svg", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", "video", "code", "pre"];
|
|
36
|
+
type ComponentsMap = {
|
|
37
|
+
[k in (typeof nativeTags)[number]]?: any;
|
|
38
|
+
};
|
|
38
39
|
export {};
|
|
39
40
|
//# sourceMappingURL=safe-mdx.d.ts.map
|
package/dist/safe-mdx.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-mdx.d.ts","sourceRoot":"","sources":["../src/safe-mdx.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"safe-mdx.d.ts","sourceRoot":"","sources":["../src/safe-mdx.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAGjD,OAAO,EAAE,IAAI,EAAQ,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAKzE,OAAO,EAAY,SAAS,EAAE,MAAM,OAAO,CAAA;AAG3C,KAAK,aAAa,GAAG,WAAW,GAAG,IAAI,CAAA;AAavC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,QAGpC;AAID,wBAAgB,eAAe,CAAC,EAC5B,UAAU,EACV,IAAS,EACT,KAAmB,GACtB;;;;CAAA,OAIA;AAED,qBAAa,UAAU;IACnB,KAAK,EAAE,aAAa,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAK;IACnB,CAAC,EAAE,aAAa,CAAA;IAChB,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAK;gBACtB,EACR,IAAS,EACT,KAAwB,EACxB,UAAgC,GACnC;;;;KAAA;IAaD,gBAAgB,CAAC,IAAI,EAAE,GAAG;IAiB1B,cAAc,CAAC,IAAI,EAAE,GAAG;IAiBxB,cAAc,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS;IAsC9C,GAAG;IAQH,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS;CAwPnD;AAED,wBAAgB,WAAW,CACvB,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,EAC3C,OAAO,GAAE,CAAC,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,KAAK,IAAoB,mBAoE9D;AAcD,wBAAgB,QAAQ,CACpB,IAAI,EAAE,MAAM,GAAG,IAAI,EACnB,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,KAAK,GAAG,SAiBpC;AAUD,QAAA,MAAM,UAAU,8VA6CN,CAAA;AA2RV,KAAK,aAAa,GAAG;KAAG,CAAC,IAAI,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG;CAAE,CAAA"}
|