@reckona/mreact-shared 0.0.66 → 0.0.67

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reckona/mreact-shared",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "description": "Shared runtime utilities used by mreact packages.",
5
5
  "keywords": [
6
6
  "jsx",
@@ -23,7 +23,8 @@
23
23
  "dist/**/*.js",
24
24
  "dist/**/*.js.map",
25
25
  "dist/**/*.d.ts",
26
- "dist/**/*.d.ts.map"
26
+ "dist/**/*.d.ts.map",
27
+ "src/**/*"
27
28
  ],
28
29
  "type": "module",
29
30
  "sideEffects": false,
@@ -0,0 +1,14 @@
1
+ export function escapeHtmlText(value: unknown): string {
2
+ return String(value)
3
+ .replaceAll("&", "&")
4
+ .replaceAll("<", "&lt;")
5
+ .replaceAll(">", "&gt;");
6
+ }
7
+
8
+ export function escapeHtmlAttribute(value: unknown): string {
9
+ return escapeHtmlText(value).replaceAll("\"", "&quot;");
10
+ }
11
+
12
+ export function escapeHtmlQuotedAttribute(value: unknown): string {
13
+ return String(value).replaceAll("&", "&amp;").replaceAll("\"", "&quot;");
14
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./html-escape.js";
2
+ export * from "./url-safety.js";
@@ -0,0 +1,115 @@
1
+ // Canonical URL and HTML-attribute safety helpers shared across server,
2
+ // React compatibility, and reactive DOM render paths.
3
+
4
+ const URL_ATTRIBUTE_NAMES = new Set([
5
+ "href",
6
+ "src",
7
+ "action",
8
+ "formaction",
9
+ "xlink:href",
10
+ "ping",
11
+ "poster",
12
+ "background",
13
+ "manifest",
14
+ ]);
15
+
16
+ const SRCSET_ATTRIBUTE_NAMES = new Set(["srcset", "imagesrcset"]);
17
+
18
+ const DANGEROUS_HTML_ATTRIBUTE_NAMES = new Set(["srcdoc"]);
19
+
20
+ const UNSAFE_URL_SCHEMES = new Set([
21
+ "javascript",
22
+ "data",
23
+ "vbscript",
24
+ "livescript",
25
+ "mhtml",
26
+ "file",
27
+ ]);
28
+
29
+ export function isDangerousHtmlAttribute(name: string): boolean {
30
+ return DANGEROUS_HTML_ATTRIBUTE_NAMES.has(name);
31
+ }
32
+
33
+ export function isDangerousHtmlOptIn(
34
+ value: unknown,
35
+ ): value is { __html: string } {
36
+ return (
37
+ typeof value === "object" &&
38
+ value !== null &&
39
+ "__html" in value &&
40
+ typeof (value as { __html?: unknown }).__html === "string"
41
+ );
42
+ }
43
+
44
+ export function isUrlAttribute(name: string): boolean {
45
+ return URL_ATTRIBUTE_NAMES.has(name);
46
+ }
47
+
48
+ export function isSrcsetAttribute(name: string): boolean {
49
+ return SRCSET_ATTRIBUTE_NAMES.has(name);
50
+ }
51
+
52
+ export function isUnsafeUrlAttribute(name: string, value: string): boolean {
53
+ if (isUrlAttribute(name)) {
54
+ return isUnsafeUrlValueForName(name, value);
55
+ }
56
+ if (isSrcsetAttribute(name)) {
57
+ const canonical = canonicalizeUrlForSchemeCheck(value);
58
+ for (const candidate of canonical.split(",")) {
59
+ const url = candidate.trim().split(/\s+/)[0] ?? "";
60
+ if (url === "") continue;
61
+ if (isUnsafeUrlValueForName("src", url)) return true;
62
+ }
63
+ return false;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ export function safeUrlAttributeValue(name: string, value: string): string | undefined {
69
+ return isUnsafeUrlAttribute(name, value) ? undefined : value;
70
+ }
71
+
72
+ export function isUnsafeMetaRefreshContent(httpEquiv: string, content: string): boolean {
73
+ if (httpEquiv.toLowerCase() !== "refresh") return false;
74
+ const match = /^[^;]*;\s*url\s*=\s*([\s\S]+)$/iu.exec(content);
75
+ if (match === null || match[1] === undefined) return false;
76
+ return isUnsafeUrlValueForName("href", stripSurroundingQuotes(match[1].trim()));
77
+ }
78
+
79
+ function stripSurroundingQuotes(value: string): string {
80
+ if (value.length < 2) return value;
81
+
82
+ const quote = value[0];
83
+ if ((quote === '"' || quote === "'") && value[value.length - 1] === quote) {
84
+ return value.slice(1, -1).trim();
85
+ }
86
+
87
+ return value;
88
+ }
89
+
90
+ function canonicalizeUrlForSchemeCheck(value: string): string {
91
+ let start = 0;
92
+
93
+ while (start < value.length && value.charCodeAt(start) <= 0x20) {
94
+ start += 1;
95
+ }
96
+
97
+ return value.slice(start).replace(/[\t\r\n]/g, "");
98
+ }
99
+
100
+ function schemeOf(value: string): string | undefined {
101
+ const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(value);
102
+ if (match === null || match[1] === undefined) return undefined;
103
+ return match[1].toLowerCase();
104
+ }
105
+
106
+ function isUnsafeUrlValueForName(name: string, value: string): boolean {
107
+ const canonical = canonicalizeUrlForSchemeCheck(value);
108
+ const scheme = schemeOf(canonical);
109
+ if (scheme === undefined) return false;
110
+ if (!UNSAFE_URL_SCHEMES.has(scheme)) return false;
111
+ if (scheme === "data" && (name === "src" || name === "poster")) {
112
+ if (/^data:image\/(?!svg\+xml(?:[;,]|$))/i.test(canonical)) return false;
113
+ }
114
+ return true;
115
+ }