@zubra/runtime 5.0.2 → 5.1.0
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/production/element.js +11 -0
- package/dist/production/index.js +1 -1
- package/dist/production/spread.js +8 -1
- package/package.json +1 -1
- package/src/element.js +11 -0
- package/src/index.js +1 -1
- package/src/spread.js +8 -1
- package/types/index.d.ts +2 -0
|
@@ -49,6 +49,12 @@ export function isSafeUrl(value) {
|
|
|
49
49
|
return ['http', 'https', 'mailto', 'tel'].includes(beforePath.slice(0, colon).toLowerCase());
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** A srcset whose every candidate's URL is safe (src/validate.rs `is_safe_srcset`). */
|
|
53
|
+
export function isSafeSrcset(value) {
|
|
54
|
+
const urls = String(value).split(',').map((candidate) => candidate.trim().split(/\s+/)[0]).filter(Boolean);
|
|
55
|
+
return urls.length > 0 && urls.every(isSafeUrl);
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
const clone = (value) => (value && typeof value === 'object' ? structuredClone(value) : value);
|
|
53
59
|
|
|
54
60
|
/** Attribute text → prop value, typed by the prop's default (JSON for arrays and objects). */
|
|
@@ -1075,6 +1081,11 @@ function bindAttribute(part, node) {
|
|
|
1075
1081
|
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
1076
1082
|
// No URL yet (a prop's "" default, before the app sets it): no attribute, and nothing to report.
|
|
1077
1083
|
if (part.url && text.trim() === '') return node.removeAttribute(name);
|
|
1084
|
+
if (part.srcset && text.trim() === '') return node.removeAttribute(name);
|
|
1085
|
+
if (part.srcset && !isSafeSrcset(text)) {
|
|
1086
|
+
console.error(`zubra: blocked an unsafe URL in "${name}"${see('safety')}:`, text);
|
|
1087
|
+
return node.removeAttribute(name);
|
|
1088
|
+
}
|
|
1078
1089
|
if (part.url && !isSafeUrl(text)) {
|
|
1079
1090
|
console.error(`zubra: blocked an unsafe URL in "${name}"${see('safety')}:`, text);
|
|
1080
1091
|
return node.removeAttribute(name);
|
package/dist/production/index.js
CHANGED
|
@@ -8,5 +8,5 @@ export {
|
|
|
8
8
|
createContext, provide, inject, ContextRequestEvent, ContextProviderEvent,
|
|
9
9
|
formValue, validity, onFormReset, onFormRestore, onFormDisabled,
|
|
10
10
|
prevent, stop, selfOnly, submit, keys, model, hasSlot, hydrated, signal, from, raw,
|
|
11
|
-
isSafeUrl, $r, $t,
|
|
11
|
+
isSafeUrl, isSafeSrcset, $r, $t,
|
|
12
12
|
} from './element.js';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// {...spread} in compiled components: an object's entries as an element's attributes,
|
|
3
3
|
// properties and listeners. Its own entry (`@zubra/runtime/spread`), which a compiled module imports
|
|
4
4
|
// only when its template spreads, so components that don't, don't load it.
|
|
5
|
-
import { $r, isSafeUrl } from './element.js';
|
|
5
|
+
import { $r, isSafeSrcset, isSafeUrl } from './element.js';
|
|
6
6
|
import { effect } from './effect.js';
|
|
7
7
|
|
|
8
8
|
const see = (section) => ` (see https://github.com/journey-to-code-org/zubra#${section})`;
|
|
@@ -130,6 +130,13 @@ export function spread(part, node) {
|
|
|
130
130
|
continue;
|
|
131
131
|
}
|
|
132
132
|
const text = String(value);
|
|
133
|
+
if (name.toLowerCase() === 'srcset') {
|
|
134
|
+
if (text.trim() === '' || !isSafeSrcset(text)) {
|
|
135
|
+
if (text.trim() !== '') console.error(`zubra: blocked an unsafe URL in "${name}"${see('safety')}:`, text);
|
|
136
|
+
node.removeAttribute(name);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
133
140
|
if (SPREAD_URLS.has(name.toLowerCase())) {
|
|
134
141
|
if (text.trim() === '') {
|
|
135
142
|
node.removeAttribute(name);
|
package/package.json
CHANGED
package/src/element.js
CHANGED
|
@@ -48,6 +48,12 @@ export function isSafeUrl(value) {
|
|
|
48
48
|
return ['http', 'https', 'mailto', 'tel'].includes(beforePath.slice(0, colon).toLowerCase());
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** A srcset whose every candidate's URL is safe (src/validate.rs `is_safe_srcset`). */
|
|
52
|
+
export function isSafeSrcset(value) {
|
|
53
|
+
const urls = String(value).split(',').map((candidate) => candidate.trim().split(/\s+/)[0]).filter(Boolean);
|
|
54
|
+
return urls.length > 0 && urls.every(isSafeUrl);
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
const clone = (value) => (value && typeof value === 'object' ? structuredClone(value) : value);
|
|
52
58
|
|
|
53
59
|
/** Attribute text → prop value, typed by the prop's default (JSON for arrays and objects). */
|
|
@@ -1090,6 +1096,11 @@ function bindAttribute(part, node) {
|
|
|
1090
1096
|
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
1091
1097
|
// No URL yet (a prop's "" default, before the app sets it): no attribute, and nothing to report.
|
|
1092
1098
|
if (part.url && text.trim() === '') return node.removeAttribute(name);
|
|
1099
|
+
if (part.srcset && text.trim() === '') return node.removeAttribute(name);
|
|
1100
|
+
if (part.srcset && !isSafeSrcset(text)) {
|
|
1101
|
+
console.error(`zubra: blocked an unsafe URL in "${name}"${see('safety')}:`, text);
|
|
1102
|
+
return node.removeAttribute(name);
|
|
1103
|
+
}
|
|
1093
1104
|
if (part.url && !isSafeUrl(text)) {
|
|
1094
1105
|
console.error(`zubra: blocked an unsafe URL in "${name}"${see('safety')}:`, text);
|
|
1095
1106
|
return node.removeAttribute(name);
|
package/src/index.js
CHANGED
|
@@ -7,5 +7,5 @@ export {
|
|
|
7
7
|
createContext, provide, inject, ContextRequestEvent, ContextProviderEvent,
|
|
8
8
|
formValue, validity, onFormReset, onFormRestore, onFormDisabled,
|
|
9
9
|
prevent, stop, selfOnly, submit, keys, model, hasSlot, hydrated, signal, from, raw,
|
|
10
|
-
isSafeUrl, $r, $t,
|
|
10
|
+
isSafeUrl, isSafeSrcset, $r, $t,
|
|
11
11
|
} from './element.js';
|
package/src/spread.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// {...spread} in compiled components: an object's entries as an element's attributes,
|
|
2
2
|
// properties and listeners. Its own entry (`@zubra/runtime/spread`), which a compiled module imports
|
|
3
3
|
// only when its template spreads, so components that don't, don't load it.
|
|
4
|
-
import { $r, isSafeUrl } from './element.js';
|
|
4
|
+
import { $r, isSafeSrcset, isSafeUrl } from './element.js';
|
|
5
5
|
import { effect } from './effect.js';
|
|
6
6
|
|
|
7
7
|
const see = (section) => ` (see https://github.com/journey-to-code-org/zubra#${section})`;
|
|
@@ -129,6 +129,13 @@ export function spread(part, node) {
|
|
|
129
129
|
continue;
|
|
130
130
|
}
|
|
131
131
|
const text = String(value);
|
|
132
|
+
if (name.toLowerCase() === 'srcset') {
|
|
133
|
+
if (text.trim() === '' || !isSafeSrcset(text)) {
|
|
134
|
+
if (text.trim() !== '') console.error(`zubra: blocked an unsafe URL in "${name}"${see('safety')}:`, text);
|
|
135
|
+
node.removeAttribute(name);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
132
139
|
if (SPREAD_URLS.has(name.toLowerCase())) {
|
|
133
140
|
if (text.trim() === '') {
|
|
134
141
|
node.removeAttribute(name);
|
package/types/index.d.ts
CHANGED
|
@@ -199,6 +199,8 @@ export declare const ABI: number;
|
|
|
199
199
|
|
|
200
200
|
/** Whether a URL is safe for `href`/`src`: relative, or http(s), mailto or tel. */
|
|
201
201
|
export declare function isSafeUrl(value: string): boolean;
|
|
202
|
+
/** True when every candidate in a srcset has a safe URL (the check a dynamic `srcset` gets). */
|
|
203
|
+
export declare function isSafeSrcset(value: string): boolean;
|
|
202
204
|
/** @internal Read a value that may be a signal. */
|
|
203
205
|
export declare function $r<T>(value: MaybeSignal<T>): T;
|
|
204
206
|
/** @internal A value as template text. */
|