pulse-js-framework 1.5.1 → 1.5.3

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/runtime/store.js CHANGED
@@ -20,6 +20,12 @@ import { loggers, createLogger } from './logger.js';
20
20
 
21
21
  const log = loggers.store;
22
22
 
23
+ /**
24
+ * Maximum nesting depth for nested objects to prevent abuse
25
+ * @type {number}
26
+ */
27
+ const MAX_NESTING_DEPTH = 10;
28
+
23
29
  /**
24
30
  * @typedef {Object} StoreOptions
25
31
  * @property {boolean} [persist=false] - Persist state to localStorage
@@ -46,6 +52,102 @@ const log = loggers.store;
46
52
  * @typedef {function(Store): Store} StorePlugin
47
53
  */
48
54
 
55
+ /**
56
+ * Dangerous property names that could cause prototype pollution
57
+ * @type {Set<string>}
58
+ */
59
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
60
+
61
+ /**
62
+ * Invalid value types that cannot be stored in state
63
+ * @type {Set<string>}
64
+ */
65
+ const INVALID_TYPES = new Set(['function', 'symbol']);
66
+
67
+ /**
68
+ * Validate state values, rejecting functions, symbols, and circular references
69
+ * @private
70
+ * @param {*} value - Value to validate
71
+ * @param {string} path - Current path for error messages
72
+ * @param {WeakSet} seen - Set of objects already visited (for circular detection)
73
+ * @throws {TypeError} If value contains invalid types or circular references
74
+ */
75
+ function validateStateValue(value, path = 'state', seen = new WeakSet()) {
76
+ const valueType = typeof value;
77
+
78
+ // Check for invalid types
79
+ if (INVALID_TYPES.has(valueType)) {
80
+ throw new TypeError(
81
+ `Invalid state value at "${path}": ${valueType}s cannot be stored in state. ` +
82
+ `State values must be primitives, arrays, or plain objects.`
83
+ );
84
+ }
85
+
86
+ // Check objects for circular references and nested invalid types
87
+ if (value !== null && valueType === 'object') {
88
+ // Check for circular reference
89
+ if (seen.has(value)) {
90
+ throw new TypeError(
91
+ `Circular reference detected at "${path}". ` +
92
+ `State must not contain circular references.`
93
+ );
94
+ }
95
+ seen.add(value);
96
+
97
+ // Validate array elements
98
+ if (Array.isArray(value)) {
99
+ for (let i = 0; i < value.length; i++) {
100
+ validateStateValue(value[i], `${path}[${i}]`, seen);
101
+ }
102
+ } else {
103
+ // Validate object properties
104
+ for (const [key, val] of Object.entries(value)) {
105
+ validateStateValue(val, `${path}.${key}`, seen);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Safely deserialize persisted state, preventing prototype pollution
113
+ * and property injection attacks.
114
+ * @private
115
+ * @param {Object} savedState - The parsed JSON state
116
+ * @param {Object} schema - The initial state defining allowed keys
117
+ * @returns {Object} Sanitized state object
118
+ */
119
+ function safeDeserialize(savedState, schema) {
120
+ if (typeof savedState !== 'object' || savedState === null || Array.isArray(savedState)) {
121
+ return {};
122
+ }
123
+
124
+ const result = {};
125
+ for (const [key, value] of Object.entries(savedState)) {
126
+ // Block dangerous keys that could pollute prototypes
127
+ if (DANGEROUS_KEYS.has(key)) {
128
+ log.warn(`Blocked potentially dangerous key in persisted state: "${key}"`);
129
+ continue;
130
+ }
131
+
132
+ // Only allow keys that exist in the schema (initial state)
133
+ if (!(key in schema)) {
134
+ continue;
135
+ }
136
+
137
+ // Recursively validate nested objects
138
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
139
+ if (typeof schema[key] === 'object' && schema[key] !== null && !Array.isArray(schema[key])) {
140
+ result[key] = safeDeserialize(value, schema[key]);
141
+ }
142
+ // If schema expects primitive but got object, skip it
143
+ } else {
144
+ result[key] = value;
145
+ }
146
+ }
147
+
148
+ return result;
149
+ }
150
+
49
151
  /**
50
152
  * Create a global store with reactive state properties.
51
153
  * @template T
@@ -70,13 +172,18 @@ const log = loggers.store;
70
172
  export function createStore(initialState = {}, options = {}) {
71
173
  const { persist = false, storageKey = 'pulse-store' } = options;
72
174
 
175
+ // Validate initial state
176
+ validateStateValue(initialState, 'initialState');
177
+
73
178
  // Load persisted state if enabled
74
179
  let state = initialState;
75
180
  if (persist && typeof localStorage !== 'undefined') {
76
181
  try {
77
182
  const saved = localStorage.getItem(storageKey);
78
183
  if (saved) {
79
- state = { ...initialState, ...JSON.parse(saved) };
184
+ const parsed = JSON.parse(saved);
185
+ const sanitized = safeDeserialize(parsed, initialState);
186
+ state = { ...initialState, ...sanitized };
80
187
  }
81
188
  } catch (e) {
82
189
  log.warn('Failed to load persisted state:', e);
@@ -93,9 +200,18 @@ export function createStore(initialState = {}, options = {}) {
93
200
  * @private
94
201
  * @param {string} key - State key
95
202
  * @param {*} value - Initial value
203
+ * @param {number} [depth=0] - Current nesting depth
96
204
  * @returns {Pulse|Object} Pulse or nested object of pulses
97
205
  */
98
- function createPulse(key, value) {
206
+ function createPulse(key, value, depth = 0) {
207
+ // Prevent excessive nesting depth
208
+ if (depth > MAX_NESTING_DEPTH) {
209
+ log.warn(`Max nesting depth (${MAX_NESTING_DEPTH}) exceeded for key: "${key}". Flattening to single pulse.`);
210
+ const p = pulse(value);
211
+ pulses[key] = p;
212
+ return p;
213
+ }
214
+
99
215
  if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
100
216
  // Create a pulse for the nested object itself (for $setState support)
101
217
  const objectPulse = pulse(value);
@@ -104,7 +220,7 @@ export function createStore(initialState = {}, options = {}) {
104
220
  // Also create nested pulses for individual properties
105
221
  const nested = {};
106
222
  for (const [k, v] of Object.entries(value)) {
107
- nested[k] = createPulse(`${key}.${k}`, v);
223
+ nested[k] = createPulse(`${key}.${k}`, v, depth + 1);
108
224
  }
109
225
  return nested;
110
226
  }
@@ -119,6 +235,15 @@ export function createStore(initialState = {}, options = {}) {
119
235
  store[key] = createPulse(key, value);
120
236
  }
121
237
 
238
+ // Sync nested pulses for persisted state to ensure consistency
239
+ if (persist && typeof localStorage !== 'undefined') {
240
+ for (const [key, value] of Object.entries(state)) {
241
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
242
+ updateNestedPulses(key, value);
243
+ }
244
+ }
245
+ }
246
+
122
247
  // Persist state changes
123
248
  if (persist) {
124
249
  effect(() => {
package/runtime/utils.js CHANGED
@@ -22,10 +22,10 @@ const HTML_ESCAPES = {
22
22
  };
23
23
 
24
24
  /**
25
- * Regex for HTML special characters
25
+ * Regex for HTML special characters (auto-generated from HTML_ESCAPES keys)
26
26
  * @private
27
27
  */
28
- const HTML_ESCAPE_REGEX = /[&<>"']/g;
28
+ const HTML_ESCAPE_REGEX = new RegExp(`[${Object.keys(HTML_ESCAPES).join('')}]`, 'g');
29
29
 
30
30
  /**
31
31
  * Escape HTML special characters to prevent XSS attacks.