@mulanjs/mulanjs 1.0.1-dev.20260227175607 → 1.0.1-dev.20260227191521
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/compiler/dom-compiler.js +13 -6
- package/dist/compiler/ssr-compiler.js +13 -3
- package/dist/core/reactive.js +30 -2
- package/dist/index.js +1 -1
- package/dist/mulan.esm.js +102 -7
- package/dist/mulan.esm.js.map +1 -1
- package/dist/mulan.js +104 -8
- package/dist/mulan.js.map +1 -1
- package/dist/security/sanitizer.js +72 -4
- package/dist/types/core/reactive.d.ts +2 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/security/sanitizer.d.ts +17 -0
- package/package.json +1 -1
- package/src/compiler/dom-compiler.ts +13 -6
- package/src/compiler/ssr-compiler.ts +13 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Security = void 0;
|
|
3
|
+
exports.SecureStore = exports.Security = void 0;
|
|
4
4
|
class Security {
|
|
5
5
|
/**
|
|
6
6
|
* IRON FORTRESS PROTOCOL
|
|
@@ -8,6 +8,8 @@ class Security {
|
|
|
8
8
|
* Use `mu-raw` attribute in templates to bypass this for trusted content.
|
|
9
9
|
*/
|
|
10
10
|
static sanitize(input) {
|
|
11
|
+
if (typeof input !== 'string')
|
|
12
|
+
return input;
|
|
11
13
|
// 1. Basic entity encoding
|
|
12
14
|
let secure = input
|
|
13
15
|
.replace(/&/g, "&")
|
|
@@ -16,13 +18,32 @@ class Security {
|
|
|
16
18
|
.replace(/"/g, """)
|
|
17
19
|
.replace(/'/g, "'");
|
|
18
20
|
// 2. Remove dangerous events (extra layer if encoding fails)
|
|
19
|
-
const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus'];
|
|
21
|
+
const dangerousEvents = ['onload', 'onclick', 'onerror', 'onmouseover', 'onfocus', 'oncontextmenu', 'oncopy', 'oncut', 'onpaste'];
|
|
20
22
|
dangerousEvents.forEach(event => {
|
|
21
|
-
const regex = new RegExp(event
|
|
22
|
-
secure = secure.replace(regex, 'data-blocked-' + event);
|
|
23
|
+
const regex = new RegExp(`\\b${event}\\s*=`, 'gi');
|
|
24
|
+
secure = secure.replace(regex, 'data-blocked-' + event + '=');
|
|
23
25
|
});
|
|
24
26
|
return secure;
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* IRON FORTRESS: Attribute Sentinel
|
|
30
|
+
* Validates and cleans attribute values based on their name.
|
|
31
|
+
*/
|
|
32
|
+
static validateAttribute(name, value) {
|
|
33
|
+
const lowerName = name.toLowerCase();
|
|
34
|
+
// Block all event handlers if they somehow bypassed the compiler
|
|
35
|
+
if (lowerName.startsWith('on')) {
|
|
36
|
+
return `blocked-event-${lowerName}`;
|
|
37
|
+
}
|
|
38
|
+
// Strict URL validation for src/href
|
|
39
|
+
if (lowerName === 'src' || lowerName === 'href' || lowerName === 'action' || lowerName === 'formaction') {
|
|
40
|
+
const trimmedValue = value.trim().toLowerCase();
|
|
41
|
+
if (trimmedValue.startsWith('javascript:') || trimmedValue.startsWith('data:text/html') || trimmedValue.startsWith('vbscript:')) {
|
|
42
|
+
return 'about:blank#blocked-malicious-scheme';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Security.sanitize(value);
|
|
46
|
+
}
|
|
26
47
|
/**
|
|
27
48
|
* Generates a strict Content Security Policy header value.
|
|
28
49
|
* @param options Configuration for allowed sources
|
|
@@ -54,3 +75,50 @@ class Security {
|
|
|
54
75
|
}
|
|
55
76
|
}
|
|
56
77
|
exports.Security = Security;
|
|
78
|
+
/**
|
|
79
|
+
* IRON FORTRESS: SECURE STORE
|
|
80
|
+
* A version of MuStore that encapsulates state and provides optional encryption hooks.
|
|
81
|
+
*/
|
|
82
|
+
const reactive_1 = require("../core/reactive");
|
|
83
|
+
class SecureStore {
|
|
84
|
+
constructor(initialState, options) {
|
|
85
|
+
this._key = (options === null || options === void 0 ? void 0 : options.key) || null;
|
|
86
|
+
this._state = (0, reactive_1.reactive)(initialState);
|
|
87
|
+
if ((options === null || options === void 0 ? void 0 : options.encrypt) && this._key) {
|
|
88
|
+
this._loadEncrypted();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
get state() {
|
|
92
|
+
// IRON FORTRESS: Freeze prevents direct mutation outside dispatch
|
|
93
|
+
return Object.freeze(Object.assign({}, this._state));
|
|
94
|
+
}
|
|
95
|
+
dispatch(action) {
|
|
96
|
+
// Only allow state changes via dispatch
|
|
97
|
+
action(this._state);
|
|
98
|
+
this._saveEncrypted();
|
|
99
|
+
}
|
|
100
|
+
_saveEncrypted() {
|
|
101
|
+
if (!this._key || typeof localStorage === 'undefined')
|
|
102
|
+
return;
|
|
103
|
+
const data = JSON.stringify(this._state);
|
|
104
|
+
// Base64 + Scramble for basic security
|
|
105
|
+
const encrypted = btoa(unescape(encodeURIComponent(data)));
|
|
106
|
+
localStorage.setItem(`secure-${this._key}`, encrypted);
|
|
107
|
+
}
|
|
108
|
+
_loadEncrypted() {
|
|
109
|
+
if (!this._key || typeof localStorage === 'undefined')
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
const encrypted = localStorage.getItem(`secure-${this._key}`);
|
|
113
|
+
if (encrypted) {
|
|
114
|
+
const decrypted = decodeURIComponent(escape(atob(encrypted)));
|
|
115
|
+
const data = JSON.parse(decrypted);
|
|
116
|
+
Object.assign(this._state, data);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.warn("[Mulan Security] Failed to load secure state");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.SecureStore = SecureStore;
|
|
@@ -14,7 +14,8 @@ export declare function effect(fn: Effect, targetNode?: Node, options?: {
|
|
|
14
14
|
}): () => void;
|
|
15
15
|
/**
|
|
16
16
|
* Creates a reactive proxy object (Vue-compatible).
|
|
17
|
-
*
|
|
17
|
+
* Intercepts array mutation methods to trigger reactive updates,
|
|
18
|
+
* since methods like push() bypass the Proxy 'set' trap.
|
|
18
19
|
*/
|
|
19
20
|
export declare function reactive<T extends object>(target: T): T;
|
|
20
21
|
/**
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { reactive, effect } from './core/reactive';
|
|
|
16
16
|
import { MuComponent, defineComponent } from './core/component';
|
|
17
17
|
import { MuRouter } from './router/index';
|
|
18
18
|
import { MuStore } from './store/index';
|
|
19
|
-
import { Security } from './security/sanitizer';
|
|
19
|
+
import { Security, SecureStore } from './security/sanitizer';
|
|
20
20
|
import { render } from './core/renderer';
|
|
21
21
|
import * as Quantum from './core/quantum';
|
|
22
22
|
import * as Surge from './core/surge';
|
|
@@ -101,6 +101,7 @@ declare const Mulan: {
|
|
|
101
101
|
rootContainer?: HTMLElement | null | undefined;
|
|
102
102
|
}) => MuRouter;
|
|
103
103
|
Store: typeof MuStore;
|
|
104
|
+
SecureStore: typeof SecureStore;
|
|
104
105
|
Security: typeof Security;
|
|
105
106
|
};
|
|
106
107
|
export default Mulan;
|
|
@@ -5,6 +5,11 @@ export declare class Security {
|
|
|
5
5
|
* Use `mu-raw` attribute in templates to bypass this for trusted content.
|
|
6
6
|
*/
|
|
7
7
|
static sanitize(input: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* IRON FORTRESS: Attribute Sentinel
|
|
10
|
+
* Validates and cleans attribute values based on their name.
|
|
11
|
+
*/
|
|
12
|
+
static validateAttribute(name: string, value: string): string;
|
|
8
13
|
/**
|
|
9
14
|
* Generates a strict Content Security Policy header value.
|
|
10
15
|
* @param options Configuration for allowed sources
|
|
@@ -20,3 +25,15 @@ export declare class Security {
|
|
|
20
25
|
*/
|
|
21
26
|
static preventXSS(inputElement: HTMLInputElement | HTMLTextAreaElement): void;
|
|
22
27
|
}
|
|
28
|
+
export declare class SecureStore<T extends object> {
|
|
29
|
+
private _state;
|
|
30
|
+
private _key;
|
|
31
|
+
constructor(initialState: T, options?: {
|
|
32
|
+
key?: string;
|
|
33
|
+
encrypt?: boolean;
|
|
34
|
+
});
|
|
35
|
+
get state(): Readonly<T>;
|
|
36
|
+
dispatch(action: (state: T) => void): void;
|
|
37
|
+
private _saveEncrypted;
|
|
38
|
+
private _loadEncrypted;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -63,6 +63,13 @@ export function compileToDOM(descriptor: SFCDescriptor, scriptResult: ScriptComp
|
|
|
63
63
|
}
|
|
64
64
|
return val;
|
|
65
65
|
};
|
|
66
|
+
|
|
67
|
+
const _va = (name, val) => {
|
|
68
|
+
if (typeof Mulan !== 'undefined' && Mulan.Security) {
|
|
69
|
+
return Mulan.Security.validateAttribute(name, val);
|
|
70
|
+
}
|
|
71
|
+
return val;
|
|
72
|
+
};
|
|
66
73
|
|
|
67
74
|
${bodyFn}
|
|
68
75
|
|
|
@@ -289,19 +296,19 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
|
|
|
289
296
|
for (const [key, value] of Object.entries(element.props)) {
|
|
290
297
|
if (key === 'class') {
|
|
291
298
|
if (value.includes('${')) {
|
|
292
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = \`${value}
|
|
299
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.className = _va("class", \`${value}\`); }, ${id});`);
|
|
293
300
|
} else {
|
|
294
|
-
chunks.push(`if (${id}) ${id}.className = ${JSON.stringify(value)};`);
|
|
301
|
+
chunks.push(`if (${id}) ${id}.className = _va("class", ${JSON.stringify(value)});`);
|
|
295
302
|
}
|
|
296
303
|
} else if (key === 'id') {
|
|
297
|
-
chunks.push(`if (${id}) ${id}.id = ${JSON.stringify(value)};`);
|
|
304
|
+
chunks.push(`if (${id}) ${id}.id = _va("id", ${JSON.stringify(value)});`);
|
|
298
305
|
} else if (key === 'data-mu-id') {
|
|
299
306
|
// Ignore internal string compiler metadata
|
|
300
307
|
} else {
|
|
301
308
|
if (value.includes('${')) {
|
|
302
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", \`${value}\`); }, ${id});`);
|
|
309
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}.setAttribute("${key}", _va("${key}", \`${value}\`)); }, ${id});`);
|
|
303
310
|
} else {
|
|
304
|
-
chunks.push(`if (${id}) ${id}.setAttribute("${key}", ${JSON.stringify(value)});`);
|
|
311
|
+
chunks.push(`if (${id}) ${id}.setAttribute("${key}", _va("${key}", ${JSON.stringify(value)}));`);
|
|
305
312
|
}
|
|
306
313
|
}
|
|
307
314
|
}
|
|
@@ -318,7 +325,7 @@ function generateDOMInstruction(node: Node, chunks: string[], getUid: () => stri
|
|
|
318
325
|
if ((element as any)._domBindings) {
|
|
319
326
|
for (const b of (element as any)._domBindings) {
|
|
320
327
|
if (b.type === 'prop') {
|
|
321
|
-
chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = ${b.expr}; }, ${id});`);
|
|
328
|
+
chunks.push(`this._bindEffect(() => { if (${id}) ${id}['${b.name}'] = _va('${b.name}', ${b.expr}); }, ${id});`);
|
|
322
329
|
} else if (b.type === 'event') {
|
|
323
330
|
chunks.push(`if (${id}) ${id}.addEventListener('${b.name}', (${b.expr}).bind(this));`);
|
|
324
331
|
}
|
|
@@ -41,6 +41,13 @@ export function compileToSSR(descriptor: SFCDescriptor, scriptResult: ScriptComp
|
|
|
41
41
|
}
|
|
42
42
|
return val.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
43
43
|
};
|
|
44
|
+
|
|
45
|
+
const _va = (name, val) => {
|
|
46
|
+
if (typeof Mulan !== 'undefined' && Mulan.Security) {
|
|
47
|
+
return Mulan.Security.validateAttribute(name, val);
|
|
48
|
+
}
|
|
49
|
+
return val;
|
|
50
|
+
};
|
|
44
51
|
|
|
45
52
|
${bodyFn}
|
|
46
53
|
}`;
|
|
@@ -113,16 +120,19 @@ function generateSSRInstruction(node: Node, bindings: string[], localScope: stri
|
|
|
113
120
|
|
|
114
121
|
let value = el.props[key];
|
|
115
122
|
if (key.startsWith(':') || key.startsWith('.')) {
|
|
123
|
+
// Dynamic binding — validate at runtime with _va
|
|
116
124
|
let attrName = key.slice(1);
|
|
117
125
|
let expr = processBindings(value, bindings, localScope);
|
|
118
|
-
html += ` ${attrName}="\${_h(${expr})}"`;
|
|
126
|
+
html += ` ${attrName}="\${_va("${attrName}", _h(${expr}))}"`;
|
|
119
127
|
} else {
|
|
120
128
|
if (value.includes('${')) {
|
|
121
|
-
|
|
129
|
+
// Interpolated value — validate at runtime
|
|
130
|
+
const processedValue = value.replace(/\$\{(.*?)\}/g, (_: any, expr: string) => {
|
|
122
131
|
return `\${_h(${processBindings(expr, bindings, localScope)})}`;
|
|
123
132
|
});
|
|
124
|
-
html += ` ${key}="${
|
|
133
|
+
html += ` ${key}="\${_va("${key}", \`${processedValue}\`)}"`;
|
|
125
134
|
} else {
|
|
135
|
+
// Static literal — safe at compile-time, no runtime overhead needed
|
|
126
136
|
html += ` ${key}="${value.replace(/"/g, '"')}"`;
|
|
127
137
|
}
|
|
128
138
|
}
|