@fynixorg/ui 1.0.12 → 1.0.14

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.
@@ -1,6 +1,98 @@
1
1
  import { transform } from "esbuild";
2
2
  import { normalizePath } from "vite";
3
3
  import * as ts from "typescript";
4
+ function sanitizeIdentifier(identifier) {
5
+ if (typeof identifier !== "string")
6
+ return "";
7
+ const sanitized = identifier.replace(/[^a-zA-Z0-9_$]/g, "");
8
+ if (/^\d/.test(sanitized)) {
9
+ return "_" + sanitized;
10
+ }
11
+ const dangerousWords = [
12
+ "eval",
13
+ "Function",
14
+ "constructor",
15
+ "prototype",
16
+ "__proto__",
17
+ "window",
18
+ "global",
19
+ "process",
20
+ "require",
21
+ "import",
22
+ "export",
23
+ "document",
24
+ "location",
25
+ "alert",
26
+ "confirm",
27
+ "prompt",
28
+ ];
29
+ if (dangerousWords.includes(sanitized)) {
30
+ return "safe_" + sanitized;
31
+ }
32
+ return sanitized || "defaultIdentifier";
33
+ }
34
+ function escapeHTML(str) {
35
+ if (typeof str !== "string")
36
+ return "";
37
+ return str
38
+ .replace(/&/g, "&")
39
+ .replace(/</g, "&lt;")
40
+ .replace(/>/g, "&gt;")
41
+ .replace(/"/g, "&quot;")
42
+ .replace(/'/g, "&#x27;")
43
+ .replace(/\//g, "&#x2F;")
44
+ .replace(/`/g, "&#x60;")
45
+ .replace(/=/g, "&#x3D;");
46
+ }
47
+ function sanitizeCSS(css) {
48
+ if (typeof css !== "string")
49
+ return "";
50
+ return css
51
+ .replace(/javascript:/gi, "")
52
+ .replace(/vbscript:/gi, "")
53
+ .replace(/data:/gi, "")
54
+ .replace(/expression\s*\(/gi, "")
55
+ .replace(/@import/gi, "")
56
+ .replace(/url\s*\(/gi, "")
57
+ .replace(/behavior\s*:/gi, "");
58
+ }
59
+ function sanitizePath(path) {
60
+ if (typeof path !== "string")
61
+ return "";
62
+ return path
63
+ .replace(/\.\./g, "")
64
+ .replace(/~/g, "")
65
+ .replace(/\\+/g, "/")
66
+ .replace(/\/+/g, "/")
67
+ .replace(/^\//g, "")
68
+ .replace(/\/$/, "");
69
+ }
70
+ function validateTemplateContent(content) {
71
+ if (typeof content !== "string")
72
+ return "";
73
+ const dangerousPatterns = [
74
+ /\$\{.*?\}/g,
75
+ /eval\s*\(/gi,
76
+ /Function\s*\(/gi,
77
+ /__proto__/gi,
78
+ /constructor/gi,
79
+ /import\s*\(/gi,
80
+ /require\s*\(/gi,
81
+ /process\./gi,
82
+ /global\./gi,
83
+ /<script/gi,
84
+ /javascript:/gi,
85
+ /vbscript:/gi,
86
+ /data:.*script/gi,
87
+ ];
88
+ for (const pattern of dangerousPatterns) {
89
+ if (pattern.test(content)) {
90
+ console.warn(`[Security] Blocked dangerous pattern in template: ${pattern}`);
91
+ return "";
92
+ }
93
+ }
94
+ return content;
95
+ }
4
96
  const colors = {
5
97
  reset: "\x1b[0m",
6
98
  red: "\x1b[31m",
@@ -13,6 +105,13 @@ const colors = {
13
105
  bold: "\x1b[1m",
14
106
  };
15
107
  function parseSFC(source) {
108
+ if (typeof source !== "string" || source.length > 1000000) {
109
+ throw new Error("Invalid or excessively large SFC source");
110
+ }
111
+ const sanitizedSource = validateTemplateContent(source);
112
+ if (!sanitizedSource) {
113
+ throw new Error("SFC contains potentially dangerous content");
114
+ }
16
115
  const result = {
17
116
  logic: "",
18
117
  view: "",
@@ -25,11 +124,14 @@ function parseSFC(source) {
25
124
  imports: [],
26
125
  exports: [],
27
126
  };
28
- const logicMatch = source.match(/<logic\s+setup\s*=\s*["']?(ts|js)["']?\s*>([\s\S]*?)<\/logic>/i);
127
+ const logicMatch = sanitizedSource.match(/<logic\s+setup\s*=\s*["']?(ts|js)["']?\s*>([\s\S]*?)<\/logic>/i);
29
128
  if (logicMatch && logicMatch[1] && logicMatch[2] !== undefined) {
30
129
  result.hasLogic = true;
31
130
  result.logicLang = logicMatch[1].toLowerCase();
32
131
  const rawLogic = logicMatch[2].trim();
132
+ if (rawLogic.length > 100000) {
133
+ throw new Error("Logic block exceeds size limit");
134
+ }
33
135
  const logicLines = rawLogic.split("\n");
34
136
  const imports = [];
35
137
  const exports = [];
@@ -40,6 +142,10 @@ function parseSFC(source) {
40
142
  for (let i = 0; i < logicLines.length; i++) {
41
143
  const line = logicLines[i] ?? "";
42
144
  const trimmed = line.trim();
145
+ if (validateTemplateContent(line) === "") {
146
+ console.warn(`[Security] Skipped potentially dangerous line in logic block`);
147
+ continue;
148
+ }
43
149
  if (inExportBlock) {
44
150
  exportBuffer.push(line);
45
151
  const openBraces = ((line && line.match(/{/g)) || []).length;
@@ -85,27 +191,61 @@ function parseSFC(source) {
85
191
  result.exports = exports;
86
192
  result.logic = otherLogic.join("\n");
87
193
  }
88
- const viewMatch = source.match(/<view\s*>([\s\S]*?)<\/view>/i);
194
+ const viewMatch = sanitizedSource.match(/<view\s*>([\s\S]*?)<\/view>/i);
89
195
  if (viewMatch && viewMatch[1] !== undefined) {
196
+ const viewContent = viewMatch[1].trim();
197
+ if (viewContent.length > 50000) {
198
+ throw new Error("View block exceeds size limit");
199
+ }
90
200
  result.hasView = true;
91
- result.view = viewMatch[1].trim();
201
+ result.view = viewContent;
92
202
  }
93
- const styleMatch = source.match(/<style(\s+scoped)?\s*>([\s\S]*?)<\/style>/i);
203
+ const styleMatch = sanitizedSource.match(/<style(\s+scoped)?\s*>([\s\S]*?)<\/style>/i);
94
204
  if (styleMatch && styleMatch[2] !== undefined) {
205
+ const styleContent = styleMatch[2].trim();
206
+ if (styleContent.length > 100000) {
207
+ throw new Error("Style block exceeds size limit");
208
+ }
95
209
  result.hasStyle = true;
96
210
  result.isStyleScoped = !!styleMatch[1];
97
- result.style = styleMatch[2].trim();
211
+ result.style = sanitizeCSS(styleContent);
98
212
  }
99
213
  return result;
100
214
  }
101
215
  function generateStyleId(filePath) {
216
+ const safePath = sanitizePath(filePath);
217
+ if (typeof crypto !== "undefined" && crypto.subtle) {
218
+ const encoder = new TextEncoder();
219
+ const data = encoder.encode(safePath + Date.now());
220
+ let dataHash = 0;
221
+ for (let i = 0; i < data.length; i++) {
222
+ const byte = data[i];
223
+ if (byte !== undefined) {
224
+ dataHash = (dataHash << 5) - dataHash + byte;
225
+ dataHash = dataHash & dataHash;
226
+ }
227
+ }
228
+ const combinedInput = safePath + "_security_salt_" + dataHash;
229
+ let hash = 0;
230
+ for (let i = 0; i < combinedInput.length; i++) {
231
+ const char = combinedInput.charCodeAt(i);
232
+ hash = (hash << 5) - hash + char;
233
+ hash = hash & hash;
234
+ }
235
+ const timestamp = Date.now().toString(36);
236
+ const hashStr = Math.abs(hash).toString(36);
237
+ return sanitizeIdentifier(`fynix-${hashStr}-${timestamp}`);
238
+ }
102
239
  let hash = 0;
103
- for (let i = 0; i < filePath.length; i++) {
104
- const char = filePath.charCodeAt(i);
240
+ const input = safePath + "_security_salt";
241
+ for (let i = 0; i < input.length; i++) {
242
+ const char = input.charCodeAt(i);
105
243
  hash = (hash << 5) - hash + char;
106
244
  hash = hash & hash;
107
245
  }
108
- return `fynix-${Math.abs(hash).toString(36)}`;
246
+ const timestamp = Date.now().toString(36);
247
+ const hashStr = Math.abs(hash).toString(36);
248
+ return sanitizeIdentifier(`fynix-${hashStr}-${timestamp}`);
109
249
  }
110
250
  function scopeStyles(css, scopeId) {
111
251
  const dataAttr = `[data-${scopeId}]`;
@@ -155,23 +295,34 @@ function scopeStyles(css, scopeId) {
155
295
  return css;
156
296
  }
157
297
  function transformSFC(parsed, filePath, jsxFactory) {
158
- const styleId = generateStyleId(filePath);
298
+ if (!parsed || typeof parsed !== "object") {
299
+ throw new Error("Invalid parsed SFC result");
300
+ }
301
+ const safeFilePath = sanitizePath(filePath);
302
+ const safeJsxFactory = sanitizeIdentifier(jsxFactory);
303
+ if (!safeJsxFactory) {
304
+ throw new Error("Invalid JSX factory name");
305
+ }
306
+ const styleId = generateStyleId(safeFilePath);
159
307
  const lines = [];
160
- lines.push(`import { ${jsxFactory} } from '@fynixorg/ui';`);
308
+ lines.push(`import { ${safeJsxFactory} } from '@fynixorg/ui';`);
161
309
  if (parsed.imports.length > 0) {
162
310
  parsed.imports.forEach((importLine) => {
163
- lines.push(importLine);
311
+ const validatedImport = validateTemplateContent(importLine);
312
+ if (validatedImport) {
313
+ lines.push(validatedImport);
314
+ }
164
315
  });
165
316
  }
166
317
  lines.push("");
167
318
  if (parsed.hasStyle) {
168
- let processedStyle = parsed.style;
319
+ let processedStyle = sanitizeCSS(parsed.style);
169
320
  if (parsed.isStyleScoped) {
170
- processedStyle = scopeStyles(parsed.style, styleId);
321
+ processedStyle = scopeStyles(processedStyle, styleId);
171
322
  }
172
323
  lines.push(`// Inject styles`);
173
324
  lines.push(`if (typeof document !== 'undefined') {`);
174
- lines.push(` const styleId = '${styleId}';`);
325
+ lines.push(` const styleId = ${JSON.stringify(styleId)};`);
175
326
  lines.push(` if (!document.getElementById(styleId)) {`);
176
327
  lines.push(` const styleEl = document.createElement('style');`);
177
328
  lines.push(` styleEl.id = styleId;`);
@@ -183,7 +334,10 @@ function transformSFC(parsed, filePath, jsxFactory) {
183
334
  }
184
335
  if (parsed.exports.length > 0) {
185
336
  parsed.exports.forEach((exportLine) => {
186
- lines.push(exportLine);
337
+ const validatedExport = validateTemplateContent(exportLine);
338
+ if (validatedExport) {
339
+ lines.push(validatedExport);
340
+ }
187
341
  });
188
342
  lines.push("");
189
343
  }
@@ -197,44 +351,58 @@ function transformSFC(parsed, filePath, jsxFactory) {
197
351
  lines.push(` // Component logic`);
198
352
  const logicLines = parsed.logic.split("\n");
199
353
  logicLines.forEach((line) => {
200
- if (line.trim()) {
201
- lines.push(` ${line}`);
354
+ const validatedLine = validateTemplateContent(line);
355
+ if (validatedLine && validatedLine.trim()) {
356
+ lines.push(` ${validatedLine}`);
202
357
  }
203
358
  });
204
359
  lines.push("");
205
360
  }
206
361
  if (parsed.exports.some((e) => e.trim().startsWith("export const meta"))) {
207
362
  lines.push(` if (typeof document !== "undefined" && typeof meta !== "undefined") {`);
208
- lines.push(` if (meta.title) document.title = meta.title;`);
363
+ lines.push(` if (meta.title) {`);
364
+ lines.push(` // Import the escapeHTML function for secure processing`);
365
+ lines.push(` const escapeHTML = ${escapeHTML.toString()};`);
366
+ lines.push(` const safeTitle = escapeHTML(String(meta.title)).substring(0, 60);`);
367
+ lines.push(` document.title = safeTitle;`);
368
+ lines.push(` }`);
209
369
  lines.push(` const _meta = meta as any;`);
370
+ lines.push(` const escapeHTML = ${escapeHTML.toString()};`);
210
371
  lines.push(` const metaTags = [`);
211
- lines.push(` _meta.description ? { name: "description", content: _meta.description } : null,`);
212
- lines.push(` _meta.keywords ? { name: "keywords", content: _meta.keywords } : null,`);
213
- lines.push(` _meta.ogTitle ? { property: "og:title", content: _meta.ogTitle } : null,`);
214
- lines.push(` _meta.ogDescription ? { property: "og:description", content: _meta.ogDescription } : null,`);
215
- lines.push(` _meta.ogImage ? { property: "og:image", content: _meta.ogImage } : null,`);
372
+ lines.push(` _meta.description ? { name: "description", content: escapeHTML(String(_meta.description || '')).substring(0, 300) } : null,`);
373
+ lines.push(` _meta.keywords ? { name: "keywords", content: escapeHTML(String(_meta.keywords || '')).substring(0, 200) } : null,`);
374
+ lines.push(` _meta.ogTitle ? { property: "og:title", content: escapeHTML(String(_meta.ogTitle || '')).substring(0, 60) } : null,`);
375
+ lines.push(` _meta.ogDescription ? { property: "og:description", content: escapeHTML(String(_meta.ogDescription || '')).substring(0, 300) } : null,`);
376
+ lines.push(` _meta.ogImage ? { property: "og:image", content: escapeHTML(String(_meta.ogImage || '')).substring(0, 500) } : null,`);
216
377
  lines.push(` ].filter(Boolean);`);
217
378
  lines.push(` metaTags.forEach((tagObj) => {`);
218
379
  lines.push(` if (!tagObj) return;`);
219
380
  lines.push(` const { name, property, content } = tagObj;`);
220
- lines.push(` if (!content) return;`);
381
+ lines.push(` if (!content || typeof content !== 'string') return;`);
221
382
  lines.push(` let tag;`);
222
383
  lines.push(` if (name) {`);
223
- lines.push(" tag = document.querySelector(`meta[name='${name}']`);");
384
+ lines.push(` const safeName = name.replace(/[^a-zA-Z0-9-_]/g, '');`);
385
+ lines.push(` if (!safeName) return;`);
386
+ lines.push(" tag = document.querySelector(`meta[name='${safeName}']`);");
224
387
  lines.push(` if (!tag) {`);
225
388
  lines.push(` tag = document.createElement("meta");`);
226
- lines.push(` tag.setAttribute("name", name);`);
389
+ lines.push(` tag.setAttribute("name", safeName);`);
227
390
  lines.push(` document.head.appendChild(tag);`);
228
391
  lines.push(` }`);
229
392
  lines.push(` } else if (property) {`);
230
- lines.push(" tag = document.querySelector(`meta[property='${property}']`);");
393
+ lines.push(` const safeProperty = property.replace(/[^a-zA-Z0-9-_:]/g, '');`);
394
+ lines.push(` if (!safeProperty) return;`);
395
+ lines.push(" tag = document.querySelector(`meta[property='${safeProperty}']`);");
231
396
  lines.push(` if (!tag) {`);
232
397
  lines.push(` tag = document.createElement("meta");`);
233
- lines.push(` tag.setAttribute("property", property);`);
398
+ lines.push(` tag.setAttribute("property", safeProperty);`);
234
399
  lines.push(` document.head.appendChild(tag);`);
235
400
  lines.push(` }`);
236
401
  lines.push(` }`);
237
- lines.push(` if (tag) tag.setAttribute("content", content);`);
402
+ lines.push(` if (tag) {`);
403
+ lines.push(` // Escape content to prevent XSS using escapeHTML function`);
404
+ lines.push(` tag.setAttribute("content", content);`);
405
+ lines.push(` }`);
238
406
  lines.push(` });`);
239
407
  lines.push(` }`);
240
408
  lines.push("");
@@ -242,7 +410,10 @@ function transformSFC(parsed, filePath, jsxFactory) {
242
410
  if (parsed.hasView) {
243
411
  lines.push(` // Component view`);
244
412
  if (parsed.isStyleScoped) {
245
- let viewContent = parsed.view.trim();
413
+ let viewContent = validateTemplateContent(parsed.view.trim());
414
+ if (!viewContent) {
415
+ throw new Error("View content contains dangerous patterns");
416
+ }
246
417
  const showGeneratedCode = typeof globalThis !== "undefined" &&
247
418
  globalThis.fynixShowGeneratedCode !== undefined
248
419
  ? globalThis.fynixShowGeneratedCode
@@ -258,15 +429,11 @@ function transformSFC(parsed, filePath, jsxFactory) {
258
429
  const closingBracket = tagMatch[3];
259
430
  if (showGeneratedCode) {
260
431
  console.log(`${colors.magenta}[DEBUG] Matched tag:${colors.reset} ${openTag}`);
261
- console.log(`${colors.magenta}[DEBUG] Attributes:${colors.reset} ${attributes}`);
262
432
  }
263
- const modifiedStart = `${openTag}${attributes} data-${styleId}=""${closingBracket}`;
433
+ const safeDataAttr = sanitizeIdentifier(`data-${styleId}`);
434
+ const modifiedStart = `${openTag}${attributes} ${safeDataAttr}=""${closingBracket}`;
264
435
  const restOfView = viewContent.substring(tagMatch[0].length);
265
436
  const modifiedView = modifiedStart + restOfView;
266
- if (showGeneratedCode) {
267
- console.log(`${colors.magenta}[DEBUG] Modified first line:${colors.reset}`);
268
- console.log(modifiedView.split("\n")[0]);
269
- }
270
437
  lines.push(` return (`);
271
438
  const viewLines = modifiedView.split("\n");
272
439
  viewLines.forEach((line) => {
@@ -276,8 +443,8 @@ function transformSFC(parsed, filePath, jsxFactory) {
276
443
  }
277
444
  else {
278
445
  lines.push(` return (`);
279
- lines.push(` <div data-${styleId}="">`);
280
- const viewLines = parsed.view.split("\n");
446
+ lines.push(` <div ${sanitizeIdentifier(`data-${styleId}`)}="">`);
447
+ const viewLines = viewContent.split("\n");
281
448
  viewLines.forEach((line) => {
282
449
  lines.push(` ${line}`);
283
450
  });
@@ -286,8 +453,12 @@ function transformSFC(parsed, filePath, jsxFactory) {
286
453
  }
287
454
  }
288
455
  else {
456
+ const validatedView = validateTemplateContent(parsed.view);
457
+ if (!validatedView) {
458
+ throw new Error("View content contains dangerous patterns");
459
+ }
289
460
  lines.push(` return (`);
290
- const viewLines = parsed.view.split("\n");
461
+ const viewLines = validatedView.split("\n");
291
462
  viewLines.forEach((line) => {
292
463
  lines.push(` ${line}`);
293
464
  });
@@ -454,6 +625,11 @@ class TypeScriptChecker {
454
625
  }
455
626
  export default function fynixPlugin(options = {}) {
456
627
  const { jsxFactory = "Fynix", jsxFragment = "Fynix.Fragment", include = [".ts", ".js", ".jsx", ".tsx", ".fnx"], exclude = ["node_modules"], sourcemap = true, esbuildOptions = {}, enableSFC = true, showGeneratedCode = false, typeCheck = false, tsConfig, } = options;
628
+ const safeJsxFactory = sanitizeIdentifier(jsxFactory);
629
+ const safeJsxFragment = sanitizeIdentifier(jsxFragment);
630
+ if (!safeJsxFactory || !safeJsxFragment) {
631
+ throw new Error("Invalid JSX factory or fragment names provided");
632
+ }
457
633
  let typeChecker = null;
458
634
  if (typeCheck) {
459
635
  typeChecker = new TypeScriptChecker(tsConfig);
@@ -463,6 +639,14 @@ export default function fynixPlugin(options = {}) {
463
639
  enforce: "pre",
464
640
  async transform(code, id) {
465
641
  const normalizedId = normalizePath(id);
642
+ const safePath = sanitizePath(normalizedId);
643
+ if (!safePath || safePath !== normalizedId.replace(/^.*[\/\\]/, "")) {
644
+ console.warn(`[Security] Potentially dangerous file path blocked: ${normalizedId}`);
645
+ return null;
646
+ }
647
+ if (code.length > 10485760) {
648
+ throw new Error(`File ${normalizedId} exceeds maximum size limit`);
649
+ }
466
650
  const shouldExclude = exclude.some((pattern) => normalizedId.includes(pattern));
467
651
  if (shouldExclude)
468
652
  return null;
@@ -477,10 +661,15 @@ export default function fynixPlugin(options = {}) {
477
661
  let codeToTransform = code;
478
662
  let loader = "tsx";
479
663
  let shouldTypeCheck = false;
664
+ const validatedCode = validateTemplateContent(code);
665
+ if (!validatedCode) {
666
+ throw new Error(`File contains potentially dangerous content: ${normalizedId}`);
667
+ }
668
+ codeToTransform = validatedCode;
480
669
  if (normalizedId.endsWith(".fnx") && enableSFC) {
481
- const parsed = parseSFC(code);
670
+ const parsed = parseSFC(codeToTransform);
482
671
  validateSFC(parsed, normalizedId);
483
- codeToTransform = transformSFC(parsed, normalizedId, jsxFactory);
672
+ codeToTransform = transformSFC(parsed, normalizedId, safeJsxFactory);
484
673
  if (showGeneratedCode) {
485
674
  console.log(`\n${colors.cyan}${"=".repeat(80)}${colors.reset}`);
486
675
  console.log(`${colors.cyan}[Fynix SFC]${colors.reset} Generated code for: ${colors.gray}${normalizedId}${colors.reset}`);
@@ -532,9 +721,12 @@ export default function fynixPlugin(options = {}) {
532
721
  }
533
722
  }
534
723
  }
724
+ if (codeToTransform.length > 5242880) {
725
+ throw new Error(`Transformed code exceeds size limit for ${normalizedId}`);
726
+ }
535
727
  const result = await transform(codeToTransform, {
536
728
  loader,
537
- jsxFactory,
729
+ jsxFactory: safeJsxFactory,
538
730
  jsxFragment,
539
731
  sourcemap,
540
732
  sourcefile: id,
@@ -585,16 +777,16 @@ export default function fynixPlugin(options = {}) {
585
777
  config() {
586
778
  const config = {
587
779
  esbuild: {
588
- jsxFactory,
589
- jsxFragment,
590
- jsxInject: `import { ${jsxFactory} } from '@fynixorg/ui'`,
780
+ jsxFactory: safeJsxFactory,
781
+ jsxFragment: safeJsxFragment,
782
+ jsxInject: `import { ${safeJsxFactory} } from '@fynixorg/ui'`,
591
783
  },
592
784
  optimizeDeps: {
593
785
  include: ["@fynixorg/ui"],
594
786
  esbuildOptions: {
595
787
  jsx: "transform",
596
- jsxFactory,
597
- jsxFragment,
788
+ jsxFactory: safeJsxFactory,
789
+ jsxFragment: safeJsxFragment,
598
790
  },
599
791
  },
600
792
  resolve: {