react-email-rails 0.4.0 → 0.4.1
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/dist/document.js +43 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/document.ts +58 -0
- package/src/version.ts +1 -1
package/dist/document.js
CHANGED
|
@@ -69,6 +69,48 @@ async function loadRenderMarkdown() {
|
|
|
69
69
|
}
|
|
70
70
|
throw new Error("marked is missing the expected parse export; check the installed version");
|
|
71
71
|
}
|
|
72
|
+
// The schema whitelists nodes and attributes but never validates URI protocols, so a
|
|
73
|
+
// javascript:/data: href on a link or button reaches content_json unchecked. Allow only safe schemes.
|
|
74
|
+
const ALLOWED_URI_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
|
|
75
|
+
// Characters browsers ignore when resolving a scheme (so "java\tscript:" runs as javascript:).
|
|
76
|
+
// Built numerically to keep the source free of literal control characters.
|
|
77
|
+
const URI_IGNORED_RANGES = [
|
|
78
|
+
[0x00, 0x20],
|
|
79
|
+
[0xa0, 0xa0],
|
|
80
|
+
[0x1680, 0x1680],
|
|
81
|
+
[0x180e, 0x180e],
|
|
82
|
+
[0x2000, 0x2029],
|
|
83
|
+
[0x205f, 0x205f],
|
|
84
|
+
[0x3000, 0x3000],
|
|
85
|
+
[0xfeff, 0xfeff],
|
|
86
|
+
];
|
|
87
|
+
const escapeCodePoint = (code) => "\\u" + code.toString(16).padStart(4, "0");
|
|
88
|
+
const URI_IGNORED_CHARS = new RegExp("[" +
|
|
89
|
+
URI_IGNORED_RANGES.map(([lo, hi]) => escapeCodePoint(lo) + "-" + escapeCodePoint(hi)).join("") +
|
|
90
|
+
"]", "g");
|
|
91
|
+
function hasAllowedUriScheme(uri) {
|
|
92
|
+
// No scheme → relative/anchor/query; nothing to neutralize.
|
|
93
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(uri.replace(URI_IGNORED_CHARS, ""))?.[1];
|
|
94
|
+
return scheme === undefined || ALLOWED_URI_SCHEMES.has(scheme.toLowerCase());
|
|
95
|
+
}
|
|
96
|
+
// Blank disallowed hrefs (link marks and nodes like button) in place; the tree is fresh
|
|
97
|
+
// toJSON() output, so mutation is safe.
|
|
98
|
+
function neutralizeUnsafeUris(value) {
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
for (const item of value)
|
|
101
|
+
neutralizeUnsafeUris(item);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (value === null || typeof value !== "object")
|
|
105
|
+
return;
|
|
106
|
+
const node = value;
|
|
107
|
+
const attrs = node.attrs;
|
|
108
|
+
if (attrs && typeof attrs.href === "string" && !hasAllowedUriScheme(attrs.href)) {
|
|
109
|
+
attrs.href = "";
|
|
110
|
+
}
|
|
111
|
+
neutralizeUnsafeUris(node.marks);
|
|
112
|
+
neutralizeUnsafeUris(node.content);
|
|
113
|
+
}
|
|
72
114
|
// Both inputs converge on HTML: markdown is rendered first, then parsed like any HTML.
|
|
73
115
|
async function resolveHtmlInput(request, dependencies) {
|
|
74
116
|
const hasHtml = request.html !== undefined;
|
|
@@ -116,6 +158,7 @@ export async function parseDocument(request, registry, dependencies = {}) {
|
|
|
116
158
|
const parseHTML = dependencies.generateJSON ?? (await loadGenerateJSON());
|
|
117
159
|
const parsed = parseHTML(html, extensions);
|
|
118
160
|
const document = schema.nodeFromJSON(parsed).toJSON();
|
|
161
|
+
neutralizeUnsafeUris(document);
|
|
119
162
|
return { document };
|
|
120
163
|
}
|
|
121
164
|
export function createParseDocument(generateJSON, renderMarkdown) {
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.4.
|
|
1
|
+
export declare const VERSION = "0.4.1";
|
|
2
2
|
export declare const RENDER_PROTOCOL_VERSION = 3;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.4.
|
|
1
|
+
export const VERSION = "0.4.1";
|
|
2
2
|
export const RENDER_PROTOCOL_VERSION = 3;
|
package/package.json
CHANGED
package/src/document.ts
CHANGED
|
@@ -105,6 +105,7 @@ async function loadRenderMarkdown(): Promise<RenderMarkdown> {
|
|
|
105
105
|
const mod = (await import(/* @vite-ignore */ "marked")) as {
|
|
106
106
|
marked?: { parse?: (markdown: string) => string | Promise<string> }
|
|
107
107
|
}
|
|
108
|
+
|
|
108
109
|
const marked = mod.marked
|
|
109
110
|
if (marked && typeof marked.parse === "function") {
|
|
110
111
|
const parse = marked.parse.bind(marked)
|
|
@@ -119,6 +120,62 @@ async function loadRenderMarkdown(): Promise<RenderMarkdown> {
|
|
|
119
120
|
throw new Error("marked is missing the expected parse export; check the installed version")
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
// The schema whitelists nodes and attributes but never validates URI protocols, so a
|
|
124
|
+
// javascript:/data: href on a link or button reaches content_json unchecked. Allow only safe schemes.
|
|
125
|
+
const ALLOWED_URI_SCHEMES: ReadonlySet<string> = new Set(["http", "https", "mailto", "tel"])
|
|
126
|
+
|
|
127
|
+
// Characters browsers ignore when resolving a scheme (so "java\tscript:" runs as javascript:).
|
|
128
|
+
// Built numerically to keep the source free of literal control characters.
|
|
129
|
+
const URI_IGNORED_RANGES: ReadonlyArray<readonly [number, number]> = [
|
|
130
|
+
[0x00, 0x20],
|
|
131
|
+
[0xa0, 0xa0],
|
|
132
|
+
[0x1680, 0x1680],
|
|
133
|
+
[0x180e, 0x180e],
|
|
134
|
+
[0x2000, 0x2029],
|
|
135
|
+
[0x205f, 0x205f],
|
|
136
|
+
[0x3000, 0x3000],
|
|
137
|
+
[0xfeff, 0xfeff],
|
|
138
|
+
]
|
|
139
|
+
const escapeCodePoint = (code: number): string => "\\u" + code.toString(16).padStart(4, "0")
|
|
140
|
+
const URI_IGNORED_CHARS = new RegExp(
|
|
141
|
+
"[" +
|
|
142
|
+
URI_IGNORED_RANGES.map(([lo, hi]) => escapeCodePoint(lo) + "-" + escapeCodePoint(hi)).join("") +
|
|
143
|
+
"]",
|
|
144
|
+
"g",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
function hasAllowedUriScheme(uri: string): boolean {
|
|
148
|
+
// No scheme → relative/anchor/query; nothing to neutralize.
|
|
149
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(uri.replace(URI_IGNORED_CHARS, ""))?.[1]
|
|
150
|
+
|
|
151
|
+
return scheme === undefined || ALLOWED_URI_SCHEMES.has(scheme.toLowerCase())
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Blank disallowed hrefs (link marks and nodes like button) in place; the tree is fresh
|
|
155
|
+
// toJSON() output, so mutation is safe.
|
|
156
|
+
function neutralizeUnsafeUris(value: unknown): void {
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
for (const item of value) neutralizeUnsafeUris(item)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (value === null || typeof value !== "object") return
|
|
163
|
+
|
|
164
|
+
const node = value as {
|
|
165
|
+
attrs?: Record<string, unknown>
|
|
166
|
+
marks?: unknown
|
|
167
|
+
content?: unknown
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const attrs = node.attrs
|
|
171
|
+
if (attrs && typeof attrs.href === "string" && !hasAllowedUriScheme(attrs.href)) {
|
|
172
|
+
attrs.href = ""
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
neutralizeUnsafeUris(node.marks)
|
|
176
|
+
neutralizeUnsafeUris(node.content)
|
|
177
|
+
}
|
|
178
|
+
|
|
122
179
|
// Both inputs converge on HTML: markdown is rendered first, then parsed like any HTML.
|
|
123
180
|
async function resolveHtmlInput(
|
|
124
181
|
request: ParseDocumentRequest,
|
|
@@ -191,6 +248,7 @@ export async function parseDocument(
|
|
|
191
248
|
const parseHTML = dependencies.generateJSON ?? (await loadGenerateJSON())
|
|
192
249
|
const parsed = parseHTML(html, extensions)
|
|
193
250
|
const document = schema.nodeFromJSON(parsed).toJSON()
|
|
251
|
+
neutralizeUnsafeUris(document)
|
|
194
252
|
|
|
195
253
|
return { document }
|
|
196
254
|
}
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.4.
|
|
1
|
+
export const VERSION = "0.4.1"
|
|
2
2
|
export const RENDER_PROTOCOL_VERSION = 3
|