pulse-js-framework 1.10.4 → 1.11.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/README.md +11 -0
- package/cli/build.js +13 -3
- package/compiler/directives.js +356 -0
- package/compiler/lexer.js +18 -3
- package/compiler/parser/core.js +6 -0
- package/compiler/parser/view.js +2 -6
- package/compiler/preprocessor.js +43 -23
- package/compiler/sourcemap.js +3 -1
- package/compiler/transformer/actions.js +329 -0
- package/compiler/transformer/export.js +7 -0
- package/compiler/transformer/expressions.js +85 -33
- package/compiler/transformer/imports.js +3 -0
- package/compiler/transformer/index.js +2 -0
- package/compiler/transformer/store.js +1 -1
- package/compiler/transformer/style.js +45 -16
- package/compiler/transformer/view.js +23 -2
- package/loader/rollup-plugin-server-components.js +391 -0
- package/loader/vite-plugin-server-components.js +420 -0
- package/loader/webpack-loader-server-components.js +356 -0
- package/package.json +124 -82
- package/runtime/async.js +4 -0
- package/runtime/context.js +16 -3
- package/runtime/dom-adapter.js +5 -3
- package/runtime/dom-virtual-list.js +2 -1
- package/runtime/form.js +8 -3
- package/runtime/graphql/cache.js +1 -1
- package/runtime/graphql/client.js +22 -0
- package/runtime/graphql/hooks.js +12 -6
- package/runtime/graphql/subscriptions.js +2 -0
- package/runtime/hmr.js +6 -3
- package/runtime/http.js +1 -0
- package/runtime/i18n.js +2 -0
- package/runtime/lru-cache.js +3 -1
- package/runtime/native.js +46 -20
- package/runtime/pulse.js +3 -0
- package/runtime/router/core.js +5 -1
- package/runtime/router/index.js +17 -1
- package/runtime/router/psc-integration.js +301 -0
- package/runtime/security.js +58 -29
- package/runtime/server-components/actions-server.js +798 -0
- package/runtime/server-components/actions.js +389 -0
- package/runtime/server-components/client.js +447 -0
- package/runtime/server-components/error-sanitizer.js +438 -0
- package/runtime/server-components/index.js +275 -0
- package/runtime/server-components/security-csrf.js +593 -0
- package/runtime/server-components/security-errors.js +227 -0
- package/runtime/server-components/security-ratelimit.js +733 -0
- package/runtime/server-components/security-validation.js +467 -0
- package/runtime/server-components/security.js +598 -0
- package/runtime/server-components/serializer.js +617 -0
- package/runtime/server-components/server.js +382 -0
- package/runtime/server-components/types.js +383 -0
- package/runtime/server-components/utils/mutex.js +60 -0
- package/runtime/server-components/utils/path-sanitizer.js +109 -0
- package/runtime/ssr.js +2 -1
- package/runtime/store.js +19 -10
- package/runtime/utils.js +12 -128
- package/types/animation.d.ts +300 -0
- package/types/i18n.d.ts +283 -0
- package/types/persistence.d.ts +267 -0
- package/types/sse.d.ts +248 -0
- package/types/sw.d.ts +150 -0
- package/runtime/a11y.js.original +0 -1844
- package/runtime/graphql.js.original +0 -1326
- package/runtime/router.js.original +0 -1605
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Server Components - Security Validation
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive security validation for props passed from Server to Client Components.
|
|
5
|
+
* Prevents secret leakage, XSS attacks, and DoS via oversized props.
|
|
6
|
+
*
|
|
7
|
+
* Security Layers:
|
|
8
|
+
* 1. Secret Detection - Detects API keys, tokens, passwords in keys/values
|
|
9
|
+
* 2. XSS Sanitization - Blocks script tags, event handlers, javascript: URLs
|
|
10
|
+
* 3. Size Validation - Enforces limits to prevent DoS attacks
|
|
11
|
+
*
|
|
12
|
+
* @module pulse-js-framework/runtime/server-components/security
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { RuntimeError } from '../errors.js';
|
|
16
|
+
import { loggers } from '../logger.js';
|
|
17
|
+
import { detectEnvironmentVariables } from './security-validation.js';
|
|
18
|
+
|
|
19
|
+
const log = loggers.dom;
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Constants - Secret Detection Patterns
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Regex patterns for detecting secrets in prop keys and values.
|
|
27
|
+
* Covers common secret naming patterns and high-entropy tokens.
|
|
28
|
+
*/
|
|
29
|
+
const SECRET_PATTERNS = [
|
|
30
|
+
// Generic key name patterns
|
|
31
|
+
/^(api[_-]?key|apikey|api[_-]?secret|apisecret)$/i,
|
|
32
|
+
/^(secret|secret[_-]?key|secretkey)$/i,
|
|
33
|
+
/^(token|auth[_-]?token|authtoken|access[_-]?token|accesstoken|refresh[_-]?token|refreshtoken)$/i,
|
|
34
|
+
/^(password|passwd|pwd)$/i,
|
|
35
|
+
/^(private[_-]?key|privatekey|priv[_-]?key|privkey)$/i,
|
|
36
|
+
/^(client[_-]?secret|clientsecret)$/i,
|
|
37
|
+
/^(session[_-]?secret|sessionsecret)$/i,
|
|
38
|
+
/^(encryption[_-]?key|encryptionkey)$/i,
|
|
39
|
+
/^(db[_-]?password|database[_-]?password|dbpassword|databasepassword)$/i,
|
|
40
|
+
/^(jwt[_-]?secret|jwtsecret)$/i,
|
|
41
|
+
/^(bearer[_-]?token|bearertoken)$/i,
|
|
42
|
+
|
|
43
|
+
// Service-specific value patterns
|
|
44
|
+
/^sk_live_[A-Za-z0-9]{24,}$/, // Stripe secret key (live)
|
|
45
|
+
/^sk_test_[A-Za-z0-9]{24,}$/, // Stripe secret key (test)
|
|
46
|
+
/^rk_live_[A-Za-z0-9]{24,}$/, // Stripe restricted key (live)
|
|
47
|
+
/^rk_test_[A-Za-z0-9]{24,}$/, // Stripe restricted key (test)
|
|
48
|
+
/^ghp_[A-Za-z0-9]{36}$/, // GitHub Personal Access Token
|
|
49
|
+
/^gho_[A-Za-z0-9]{36}$/, // GitHub OAuth Token
|
|
50
|
+
/^ghs_[A-Za-z0-9]{36}$/, // GitHub Server Token
|
|
51
|
+
/^github_pat_[A-Za-z0-9_]{82}$/, // GitHub Fine-grained PAT
|
|
52
|
+
|
|
53
|
+
// Generic high-entropy patterns (likely secrets)
|
|
54
|
+
/^[A-Za-z0-9+/]{40,}={0,2}$/, // Base64 40+ chars (likely token)
|
|
55
|
+
/^[A-Z0-9]{32,}$/, // All-caps alphanumeric 32+ chars
|
|
56
|
+
/^[a-f0-9]{64}$/, // 64-char hex (SHA-256, could be secret)
|
|
57
|
+
/^[a-f0-9]{128}$/, // 128-char hex (SHA-512)
|
|
58
|
+
|
|
59
|
+
// PEM-encoded private keys
|
|
60
|
+
/-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
|
61
|
+
/-----BEGIN ENCRYPTED PRIVATE KEY-----/
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* XSS detection patterns for string prop values.
|
|
66
|
+
*/
|
|
67
|
+
const XSS_PATTERNS = {
|
|
68
|
+
scriptTag: /<script[\s>]/i,
|
|
69
|
+
eventHandler: /<[^>]+on\w+\s*=/i,
|
|
70
|
+
javascriptProtocol: /javascript:/i,
|
|
71
|
+
vbscriptProtocol: /vbscript:/i,
|
|
72
|
+
dataHtmlProtocol: /data:text\/html/i
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Constants - Size Limits
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Size limits to prevent DoS attacks via oversized props.
|
|
81
|
+
* These limits are generous for normal use but prevent abuse.
|
|
82
|
+
*/
|
|
83
|
+
export const PROP_SIZE_LIMITS = {
|
|
84
|
+
MAX_DEPTH: 20, // Max object nesting depth
|
|
85
|
+
MAX_STRING_LENGTH: 100_000, // 100KB per string value
|
|
86
|
+
MAX_ARRAY_LENGTH: 10_000, // 10K items per array
|
|
87
|
+
MAX_OBJECT_KEYS: 1_000, // 1K keys per object
|
|
88
|
+
MAX_TOTAL_SIZE: 1_000_000 // 1MB total JSON serialized size
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Secret Detection
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detected secret information.
|
|
97
|
+
*
|
|
98
|
+
* @typedef {Object} DetectedSecret
|
|
99
|
+
* @property {string} path - Path to the secret (e.g., 'props.user.apiKey')
|
|
100
|
+
* @property {string} value - Truncated value preview (first 20 chars)
|
|
101
|
+
* @property {string} pattern - Regex pattern that matched
|
|
102
|
+
* @property {'key'|'value'} type - Whether secret was in key name or value
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Scan object for potential secrets (API keys, tokens, passwords).
|
|
107
|
+
*
|
|
108
|
+
* Detects secrets in both property keys and values using pattern matching.
|
|
109
|
+
* Returns a list of detected secrets for logging/warning purposes.
|
|
110
|
+
*
|
|
111
|
+
* @param {any} obj - Object to scan for secrets
|
|
112
|
+
* @param {string} [path='props'] - Current path for error messages
|
|
113
|
+
* @returns {DetectedSecret[]} Array of detected secrets
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const secrets = detectSecrets({ apiKey: 'sk_live_abc123...' });
|
|
117
|
+
* // [{ path: 'props.apiKey', value: 'sk_live_abc123...', pattern: '/^sk_live_/', type: 'value' }]
|
|
118
|
+
*/
|
|
119
|
+
export function detectSecrets(obj, path = 'props') {
|
|
120
|
+
const detected = [];
|
|
121
|
+
const seen = new WeakSet();
|
|
122
|
+
|
|
123
|
+
function scan(value, currentPath) {
|
|
124
|
+
if (value === null || value === undefined) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const type = typeof value;
|
|
129
|
+
|
|
130
|
+
// Check string values against value patterns
|
|
131
|
+
if (type === 'string') {
|
|
132
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
133
|
+
if (pattern.test(value)) {
|
|
134
|
+
detected.push({
|
|
135
|
+
path: currentPath,
|
|
136
|
+
value: value.length > 20 ? value.substring(0, 20) + '...' : value,
|
|
137
|
+
pattern: pattern.toString(),
|
|
138
|
+
type: 'value'
|
|
139
|
+
});
|
|
140
|
+
break; // One match per value is enough
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check objects and arrays recursively
|
|
146
|
+
if (type === 'object') {
|
|
147
|
+
// Circular reference check
|
|
148
|
+
if (seen.has(value)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
seen.add(value);
|
|
152
|
+
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
value.forEach((item, i) => scan(item, `${currentPath}[${i}]`));
|
|
155
|
+
} else {
|
|
156
|
+
// Check each key and value
|
|
157
|
+
for (const [key, val] of Object.entries(value)) {
|
|
158
|
+
// Check KEY names against patterns
|
|
159
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
160
|
+
if (pattern.test(key)) {
|
|
161
|
+
detected.push({
|
|
162
|
+
path: `${currentPath}.${key}`,
|
|
163
|
+
value: typeof val === 'string'
|
|
164
|
+
? (val.length > 20 ? val.substring(0, 20) + '...' : val)
|
|
165
|
+
: String(val),
|
|
166
|
+
pattern: pattern.toString(),
|
|
167
|
+
type: 'key'
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Scan value recursively
|
|
174
|
+
scan(val, `${currentPath}.${key}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
scan(obj, path);
|
|
181
|
+
return detected;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// XSS Sanitization
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Sanitize props for XSS attacks before serialization.
|
|
190
|
+
*
|
|
191
|
+
* Detects and neutralizes:
|
|
192
|
+
* - Script tags (<script>)
|
|
193
|
+
* - Event handlers (onclick=, onerror=, etc.)
|
|
194
|
+
* - javascript: and vbscript: protocols
|
|
195
|
+
* - data:text/html URLs
|
|
196
|
+
*
|
|
197
|
+
* Logs warnings for each detected pattern. Mutates the props object in place
|
|
198
|
+
* for performance (avoids deep cloning).
|
|
199
|
+
*
|
|
200
|
+
* @param {any} props - Props object to sanitize
|
|
201
|
+
* @param {string} componentId - Component ID for logging
|
|
202
|
+
* @returns {any} Sanitized props (mutated in place)
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* const props = { html: '<script>alert("xss")</script>' };
|
|
206
|
+
* sanitizePropsForXSS(props, 'MyComponent');
|
|
207
|
+
* // props.html is now: '<script>alert("xss")</script>'
|
|
208
|
+
*/
|
|
209
|
+
export function sanitizePropsForXSS(props, componentId) {
|
|
210
|
+
const seen = new WeakSet();
|
|
211
|
+
|
|
212
|
+
function sanitize(value, path) {
|
|
213
|
+
if (value === null || value === undefined) {
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const type = typeof value;
|
|
218
|
+
|
|
219
|
+
// Sanitize strings
|
|
220
|
+
if (type === 'string') {
|
|
221
|
+
let sanitized = value;
|
|
222
|
+
let modified = false;
|
|
223
|
+
|
|
224
|
+
// Check for script tags
|
|
225
|
+
if (XSS_PATTERNS.scriptTag.test(sanitized)) {
|
|
226
|
+
log.warn(`PSC: Script tag detected in prop '${path}' for '${componentId}'`);
|
|
227
|
+
sanitized = sanitized.replace(/<script[\s>]/gi, '<script');
|
|
228
|
+
modified = true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check for event handlers in HTML-like content
|
|
232
|
+
if (XSS_PATTERNS.eventHandler.test(sanitized)) {
|
|
233
|
+
log.warn(`PSC: Event handler detected in prop '${path}' for '${componentId}'`);
|
|
234
|
+
sanitized = sanitized.replace(/on\w+\s*=/gi, 'data-blocked-event=');
|
|
235
|
+
modified = true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for javascript: protocol
|
|
239
|
+
if (XSS_PATTERNS.javascriptProtocol.test(sanitized)) {
|
|
240
|
+
log.warn(`PSC: javascript: protocol detected in prop '${path}' for '${componentId}'`);
|
|
241
|
+
sanitized = sanitized.replace(/javascript:/gi, 'blocked:');
|
|
242
|
+
modified = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check for vbscript: protocol
|
|
246
|
+
if (XSS_PATTERNS.vbscriptProtocol.test(sanitized)) {
|
|
247
|
+
log.warn(`PSC: vbscript: protocol detected in prop '${path}' for '${componentId}'`);
|
|
248
|
+
sanitized = sanitized.replace(/vbscript:/gi, 'blocked:');
|
|
249
|
+
modified = true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Check for data:text/html (can execute scripts)
|
|
253
|
+
if (XSS_PATTERNS.dataHtmlProtocol.test(sanitized)) {
|
|
254
|
+
log.warn(`PSC: data:text/html protocol detected in prop '${path}' for '${componentId}'`);
|
|
255
|
+
sanitized = sanitized.replace(/data:text\/html/gi, 'data:text/plain');
|
|
256
|
+
modified = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return sanitized;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Recursively sanitize objects and arrays
|
|
263
|
+
if (type === 'object') {
|
|
264
|
+
if (seen.has(value)) {
|
|
265
|
+
return value; // Already processed (circular ref)
|
|
266
|
+
}
|
|
267
|
+
seen.add(value);
|
|
268
|
+
|
|
269
|
+
if (Array.isArray(value)) {
|
|
270
|
+
for (let i = 0; i < value.length; i++) {
|
|
271
|
+
value[i] = sanitize(value[i], `${path}[${i}]`);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
for (const [key, val] of Object.entries(value)) {
|
|
275
|
+
value[key] = sanitize(val, `${path}.${key}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return value;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return sanitize(props, 'props');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================================
|
|
289
|
+
// Size Validation
|
|
290
|
+
// ============================================================================
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Validate prop size limits to prevent DoS attacks.
|
|
294
|
+
*
|
|
295
|
+
* Enforces limits on:
|
|
296
|
+
* - Nesting depth (max 20 levels)
|
|
297
|
+
* - String length (max 100KB per string)
|
|
298
|
+
* - Array length (max 10K items)
|
|
299
|
+
* - Object keys (max 1K keys per object)
|
|
300
|
+
* - Total JSON size (max 1MB)
|
|
301
|
+
*
|
|
302
|
+
* Throws RuntimeError if any limit is exceeded.
|
|
303
|
+
*
|
|
304
|
+
* @param {any} props - Props to validate
|
|
305
|
+
* @param {string} componentId - Component ID for error messages
|
|
306
|
+
* @throws {RuntimeError} If size limits exceeded
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* validatePropSizeLimits({ data: 'x'.repeat(200000) }, 'MyComponent');
|
|
310
|
+
* // Throws: RuntimeError('String prop too large...')
|
|
311
|
+
*/
|
|
312
|
+
export function validatePropSizeLimits(props, componentId) {
|
|
313
|
+
let totalSize = 0;
|
|
314
|
+
const seen = new WeakSet();
|
|
315
|
+
|
|
316
|
+
function check(value, path, depth) {
|
|
317
|
+
// Check nesting depth
|
|
318
|
+
if (depth > PROP_SIZE_LIMITS.MAX_DEPTH) {
|
|
319
|
+
throw new RuntimeError(
|
|
320
|
+
`PSC: Prop nesting depth exceeded ${PROP_SIZE_LIMITS.MAX_DEPTH} at '${path}'`,
|
|
321
|
+
{
|
|
322
|
+
code: 'PSC_PROP_DEPTH_EXCEEDED',
|
|
323
|
+
context: `Component: ${componentId}`,
|
|
324
|
+
suggestion: 'Flatten your data structure or split into smaller props'
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (value === null || value === undefined) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const type = typeof value;
|
|
334
|
+
|
|
335
|
+
// Check string length
|
|
336
|
+
if (type === 'string') {
|
|
337
|
+
totalSize += value.length;
|
|
338
|
+
if (value.length > PROP_SIZE_LIMITS.MAX_STRING_LENGTH) {
|
|
339
|
+
throw new RuntimeError(
|
|
340
|
+
`PSC: String prop too large at '${path}' (${value.length} > ${PROP_SIZE_LIMITS.MAX_STRING_LENGTH})`,
|
|
341
|
+
{
|
|
342
|
+
code: 'PSC_STRING_TOO_LARGE',
|
|
343
|
+
context: `Component: ${componentId}`,
|
|
344
|
+
suggestion: 'Split large strings into chunks or store externally'
|
|
345
|
+
}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check numbers (count toward size)
|
|
351
|
+
if (type === 'number') {
|
|
352
|
+
totalSize += 8; // Approximate size
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Check boolean (count toward size)
|
|
356
|
+
if (type === 'boolean') {
|
|
357
|
+
totalSize += 4;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Check objects and arrays
|
|
361
|
+
if (type === 'object' && value !== null) {
|
|
362
|
+
if (seen.has(value)) {
|
|
363
|
+
return; // Already counted (circular ref)
|
|
364
|
+
}
|
|
365
|
+
seen.add(value);
|
|
366
|
+
|
|
367
|
+
if (Array.isArray(value)) {
|
|
368
|
+
// Check array length
|
|
369
|
+
if (value.length > PROP_SIZE_LIMITS.MAX_ARRAY_LENGTH) {
|
|
370
|
+
throw new RuntimeError(
|
|
371
|
+
`PSC: Array too large at '${path}' (${value.length} > ${PROP_SIZE_LIMITS.MAX_ARRAY_LENGTH})`,
|
|
372
|
+
{
|
|
373
|
+
code: 'PSC_ARRAY_TOO_LARGE',
|
|
374
|
+
context: `Component: ${componentId}`,
|
|
375
|
+
suggestion: 'Use pagination or split into smaller arrays'
|
|
376
|
+
}
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check each item recursively
|
|
381
|
+
value.forEach((item, i) => check(item, `${path}[${i}]`, depth + 1));
|
|
382
|
+
} else {
|
|
383
|
+
// Check object key count
|
|
384
|
+
const keys = Object.keys(value);
|
|
385
|
+
if (keys.length > PROP_SIZE_LIMITS.MAX_OBJECT_KEYS) {
|
|
386
|
+
throw new RuntimeError(
|
|
387
|
+
`PSC: Object has too many keys at '${path}' (${keys.length} > ${PROP_SIZE_LIMITS.MAX_OBJECT_KEYS})`,
|
|
388
|
+
{
|
|
389
|
+
code: 'PSC_OBJECT_TOO_LARGE',
|
|
390
|
+
context: `Component: ${componentId}`,
|
|
391
|
+
suggestion: 'Split into smaller objects or use a Map/Set'
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check each property recursively
|
|
397
|
+
for (const [key, val] of Object.entries(value)) {
|
|
398
|
+
totalSize += key.length; // Count key names too
|
|
399
|
+
check(val, `${path}.${key}`, depth + 1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check total depth and size
|
|
406
|
+
check(props, 'props', 0);
|
|
407
|
+
|
|
408
|
+
// Check total JSON size (approximate via stringification)
|
|
409
|
+
// Note: Circular references are already caught by check() via WeakSet,
|
|
410
|
+
// but JSON.stringify() can still throw if circular refs exist.
|
|
411
|
+
// If we reach here, props should be serializable.
|
|
412
|
+
try {
|
|
413
|
+
const jsonSize = JSON.stringify(props).length;
|
|
414
|
+
if (jsonSize > PROP_SIZE_LIMITS.MAX_TOTAL_SIZE) {
|
|
415
|
+
throw new RuntimeError(
|
|
416
|
+
`PSC: Total prop size too large (${jsonSize} bytes > ${PROP_SIZE_LIMITS.MAX_TOTAL_SIZE} bytes)`,
|
|
417
|
+
{
|
|
418
|
+
code: 'PSC_PROPS_TOO_LARGE',
|
|
419
|
+
context: `Component: ${componentId}`,
|
|
420
|
+
suggestion: 'Reduce prop size or fetch data on the client'
|
|
421
|
+
}
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
// If JSON.stringify throws (e.g., circular reference), throw specific error
|
|
426
|
+
if (err.message && err.message.includes('circular')) {
|
|
427
|
+
throw new RuntimeError(
|
|
428
|
+
`PSC: Props contain circular reference at '${componentId}'`,
|
|
429
|
+
{
|
|
430
|
+
code: 'PSC_CIRCULAR_PROP',
|
|
431
|
+
context: 'Props must not contain circular references',
|
|
432
|
+
suggestion: 'Check for circular object references in your props'
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
// Re-throw if it's our own RuntimeError (size limit exceeded)
|
|
437
|
+
if (err.code && err.code.startsWith('PSC_')) {
|
|
438
|
+
throw err;
|
|
439
|
+
}
|
|
440
|
+
// Unknown error during stringification
|
|
441
|
+
throw new RuntimeError(
|
|
442
|
+
`PSC: Failed to serialize props for '${componentId}'`,
|
|
443
|
+
{
|
|
444
|
+
code: 'PSC_SERIALIZATION_FAILED',
|
|
445
|
+
context: err.message
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ============================================================================
|
|
452
|
+
// Main Security Validator
|
|
453
|
+
// ============================================================================
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Security validation result.
|
|
457
|
+
*
|
|
458
|
+
* @typedef {Object} SecurityValidationResult
|
|
459
|
+
* @property {boolean} valid - True if no blocking errors found
|
|
460
|
+
* @property {DetectedSecret[]} warnings - Detected secrets (warnings, not blocking)
|
|
461
|
+
* @property {Error[]} errors - Validation errors (blocking)
|
|
462
|
+
* @property {any} sanitized - Sanitized props (XSS patterns removed)
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Validate prop security comprehensively.
|
|
467
|
+
*
|
|
468
|
+
* Orchestrates all security validations:
|
|
469
|
+
* 1. Secret detection (warns but doesn't block)
|
|
470
|
+
* 2. XSS sanitization (sanitizes and warns)
|
|
471
|
+
* 3. Size validation (throws on violation)
|
|
472
|
+
* 4. Environment variable detection (warns but doesn't block) - NEW
|
|
473
|
+
*
|
|
474
|
+
* @param {any} props - Props to validate
|
|
475
|
+
* @param {string} componentId - Component ID for logging
|
|
476
|
+
* @param {Object} [options] - Validation options
|
|
477
|
+
* @param {boolean} [options.detectSecrets=true] - Enable secret detection
|
|
478
|
+
* @param {boolean} [options.sanitizeXSS=true] - Enable XSS sanitization
|
|
479
|
+
* @param {boolean} [options.validateSizes=true] - Enable size validation
|
|
480
|
+
* @param {boolean} [options.detectEnvVars=true] - Enable environment variable detection
|
|
481
|
+
* @param {boolean} [options.throwOnSecrets=false] - Throw error if secrets detected (default: warn only)
|
|
482
|
+
* @returns {SecurityValidationResult} Validation result
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* const result = validatePropSecurity(props, 'MyComponent');
|
|
486
|
+
* if (!result.valid) {
|
|
487
|
+
* console.error('Validation failed:', result.errors);
|
|
488
|
+
* }
|
|
489
|
+
* if (result.warnings.length > 0) {
|
|
490
|
+
* console.warn('Detected secrets:', result.warnings);
|
|
491
|
+
* }
|
|
492
|
+
* return result.sanitized;
|
|
493
|
+
*/
|
|
494
|
+
export function validatePropSecurity(props, componentId, options = {}) {
|
|
495
|
+
const {
|
|
496
|
+
detectSecrets: enableSecretDetection = true,
|
|
497
|
+
sanitizeXSS: enableXSSSanitization = true,
|
|
498
|
+
validateSizes: enableSizeValidation = true,
|
|
499
|
+
detectEnvVars = true,
|
|
500
|
+
throwOnSecrets = false
|
|
501
|
+
} = options;
|
|
502
|
+
|
|
503
|
+
const warnings = [];
|
|
504
|
+
const errors = [];
|
|
505
|
+
let sanitized = props;
|
|
506
|
+
|
|
507
|
+
// 1. Detect secrets (warns only, doesn't modify props)
|
|
508
|
+
if (enableSecretDetection) {
|
|
509
|
+
try {
|
|
510
|
+
const secrets = detectSecrets(props);
|
|
511
|
+
if (secrets.length > 0) {
|
|
512
|
+
warnings.push(...secrets);
|
|
513
|
+
log.warn(
|
|
514
|
+
`PSC: Detected ${secrets.length} potential secret(s) in props for '${componentId}'`,
|
|
515
|
+
secrets
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
if (throwOnSecrets) {
|
|
519
|
+
throw new RuntimeError(
|
|
520
|
+
`PSC: Secrets detected in props for '${componentId}'`,
|
|
521
|
+
{
|
|
522
|
+
code: 'PSC_SECRETS_IN_PROPS',
|
|
523
|
+
context: `Detected ${secrets.length} secret pattern(s)`,
|
|
524
|
+
suggestion: 'Remove sensitive data from props or use Server Actions',
|
|
525
|
+
details: secrets
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
errors.push(err);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 2. Sanitize XSS (modifies props in place)
|
|
536
|
+
if (enableXSSSanitization) {
|
|
537
|
+
try {
|
|
538
|
+
sanitized = sanitizePropsForXSS(sanitized, componentId);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
errors.push(err);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 3. Validate sizes (throws on violation)
|
|
545
|
+
if (enableSizeValidation) {
|
|
546
|
+
try {
|
|
547
|
+
validatePropSizeLimits(sanitized, componentId);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
errors.push(err);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 4. Detect environment variables (warns only, doesn't modify props) - NEW
|
|
554
|
+
if (detectEnvVars) {
|
|
555
|
+
try {
|
|
556
|
+
const envVarResult = detectEnvironmentVariables(sanitized, 'props');
|
|
557
|
+
|
|
558
|
+
if (envVarResult.detected) {
|
|
559
|
+
// Add env var detections to warnings with a specific type
|
|
560
|
+
const envVarWarnings = envVarResult.warnings.map(w => ({
|
|
561
|
+
...w,
|
|
562
|
+
type: 'env-var', // Mark as env-var warning (different from secret warning)
|
|
563
|
+
severity: 'warning'
|
|
564
|
+
}));
|
|
565
|
+
warnings.push(...envVarWarnings);
|
|
566
|
+
|
|
567
|
+
log.warn(
|
|
568
|
+
`PSC: Environment variable(s) detected in props for '${componentId}':`,
|
|
569
|
+
envVarResult.warnings.map(w => `${w.path}: ${w.pattern} (${w.platform})`)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
} catch (err) {
|
|
573
|
+
// Don't block on env var detection errors (validation is optional)
|
|
574
|
+
log.warn(`PSC: Environment variable detection failed for '${componentId}':`, err.message);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
valid: errors.length === 0,
|
|
580
|
+
warnings,
|
|
581
|
+
errors,
|
|
582
|
+
sanitized
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ============================================================================
|
|
587
|
+
// Exports
|
|
588
|
+
// ============================================================================
|
|
589
|
+
|
|
590
|
+
export default {
|
|
591
|
+
detectSecrets,
|
|
592
|
+
sanitizePropsForXSS,
|
|
593
|
+
validatePropSizeLimits,
|
|
594
|
+
validatePropSecurity,
|
|
595
|
+
PROP_SIZE_LIMITS,
|
|
596
|
+
SECRET_PATTERNS,
|
|
597
|
+
XSS_PATTERNS
|
|
598
|
+
};
|