@arcis/node 1.4.0 → 1.4.2

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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/core/constants.d.ts +2 -2
  3. package/dist/core/constants.d.ts.map +1 -1
  4. package/dist/core/index.js +11 -3
  5. package/dist/core/index.js.map +1 -1
  6. package/dist/core/index.mjs +11 -3
  7. package/dist/core/index.mjs.map +1 -1
  8. package/dist/index.js +125 -46
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +126 -47
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/logging/index.js.map +1 -1
  13. package/dist/logging/index.mjs.map +1 -1
  14. package/dist/middleware/csrf.d.ts.map +1 -1
  15. package/dist/middleware/index.js +62 -30
  16. package/dist/middleware/index.js.map +1 -1
  17. package/dist/middleware/index.mjs +63 -31
  18. package/dist/middleware/index.mjs.map +1 -1
  19. package/dist/middleware/rate-limit.d.ts.map +1 -1
  20. package/dist/sanitizers/encode.d.ts.map +1 -1
  21. package/dist/sanitizers/index.d.ts +1 -0
  22. package/dist/sanitizers/index.d.ts.map +1 -1
  23. package/dist/sanitizers/index.js +90 -32
  24. package/dist/sanitizers/index.js.map +1 -1
  25. package/dist/sanitizers/index.mjs +88 -33
  26. package/dist/sanitizers/index.mjs.map +1 -1
  27. package/dist/sanitizers/ldap.d.ts +42 -0
  28. package/dist/sanitizers/ldap.d.ts.map +1 -0
  29. package/dist/sanitizers/path.d.ts.map +1 -1
  30. package/dist/sanitizers/sanitize.d.ts.map +1 -1
  31. package/dist/sanitizers/ssti.d.ts.map +1 -1
  32. package/dist/stores/index.js +21 -1
  33. package/dist/stores/index.js.map +1 -1
  34. package/dist/stores/index.mjs +21 -1
  35. package/dist/stores/index.mjs.map +1 -1
  36. package/dist/stores/memory.d.ts +4 -10
  37. package/dist/stores/memory.d.ts.map +1 -1
  38. package/dist/validation/index.js +38 -21
  39. package/dist/validation/index.js.map +1 -1
  40. package/dist/validation/index.mjs +38 -21
  41. package/dist/validation/index.mjs.map +1 -1
  42. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -70,7 +70,15 @@ var XSS_PATTERNS = [
70
70
  /** URL-encoded script tags */
71
71
  /%3Cscript/gi,
72
72
  /** SVG with onload */
73
- /<svg[^>]*onload/gi
73
+ /<svg[^>]*onload/gi,
74
+ /** form tags — phishing/credential harvesting via action= redirection */
75
+ /<form[\s>]/gi,
76
+ /** meta tags — http-equiv refresh redirects or CSP bypass */
77
+ /<meta[\s>]/gi,
78
+ /** base href hijacking — redirects all relative URLs to attacker domain */
79
+ /<base[\s>]/gi,
80
+ /** link tag injection — stylesheet or preload CSRF attacks */
81
+ /<link[\s>]/gi
74
82
  ];
75
83
  var XSS_REMOVE_PATTERNS = [
76
84
  /** Full script blocks (content + tags) */
@@ -97,7 +105,15 @@ var XSS_REMOVE_PATTERNS = [
97
105
  /javascript\s*:/gi,
98
106
  /vbscript\s*:/gi,
99
107
  /** data: URIs with HTML/script content */
100
- /data\s*:\s*text\/html[^>\s]*/gi
108
+ /data\s*:\s*text\/html[^>\s]*/gi,
109
+ /** form tag injection — phishing via action= redirection */
110
+ /<form[\s>][^>]*/gi,
111
+ /** meta tag injection — http-equiv refresh or CSP bypass */
112
+ /<meta[\s>][^>]*/gi,
113
+ /** base href hijacking */
114
+ /<base[\s>][^>]*/gi,
115
+ /** link tag injection — stylesheet or preload attacks */
116
+ /<link[\s>][^>]*/gi
101
117
  ];
102
118
  var SQL_PATTERNS = [
103
119
  /** SQL keywords */
@@ -161,8 +177,8 @@ var COMMAND_PATTERNS = [
161
177
  /[;&|`]/g,
162
178
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
163
179
  /\$\(/g,
164
- /** URL-encoded newline/carriage-return injection (%0a, %0d) */
165
- /%0[ad]/gi
180
+ /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
181
+ /%0[0-9a-f]/gi
166
182
  ];
167
183
  var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
168
184
  "__proto__",
@@ -453,7 +469,24 @@ function createRateLimiter(options = {}) {
453
469
  }
454
470
  next();
455
471
  } catch (error) {
456
- console.error("[arcis] Rate limiter error:", error);
472
+ console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
473
+ try {
474
+ const key = keyGenerator(req);
475
+ const now = Date.now();
476
+ if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
477
+ inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
478
+ } else {
479
+ inMemoryStore[key].count++;
480
+ }
481
+ const count = inMemoryStore[key].count;
482
+ if (count > max) {
483
+ const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
484
+ res.setHeader("Retry-After", resetSeconds.toString());
485
+ res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
486
+ return;
487
+ }
488
+ } catch {
489
+ }
457
490
  next();
458
491
  }
459
492
  };
@@ -697,26 +730,31 @@ function sanitizePath(input, collectThreats = false) {
697
730
  const threats = [];
698
731
  let value = input;
699
732
  let wasSanitized = false;
700
- for (const pattern of PATH_PATTERNS) {
701
- pattern.lastIndex = 0;
702
- if (pattern.test(value)) {
733
+ value = value.normalize("NFKC");
734
+ let prev;
735
+ do {
736
+ prev = value;
737
+ for (const pattern of PATH_PATTERNS) {
703
738
  pattern.lastIndex = 0;
704
- if (collectThreats) {
705
- const matches = value.match(pattern);
706
- if (matches) {
707
- for (const match of matches) {
708
- threats.push({
709
- type: "path_traversal",
710
- pattern: pattern.source,
711
- original: match
712
- });
739
+ if (pattern.test(value)) {
740
+ pattern.lastIndex = 0;
741
+ if (collectThreats) {
742
+ const matches = value.match(pattern);
743
+ if (matches) {
744
+ for (const match of matches) {
745
+ threats.push({
746
+ type: "path_traversal",
747
+ pattern: pattern.source,
748
+ original: match
749
+ });
750
+ }
713
751
  }
714
752
  }
753
+ value = value.replace(pattern, "");
754
+ wasSanitized = true;
715
755
  }
716
- value = value.replace(pattern, "");
717
- wasSanitized = true;
718
756
  }
719
- }
757
+ } while (value !== prev);
720
758
  if (collectThreats) {
721
759
  return { value, wasSanitized, threats };
722
760
  }
@@ -724,9 +762,10 @@ function sanitizePath(input, collectThreats = false) {
724
762
  }
725
763
  function detectPathTraversal(input) {
726
764
  if (typeof input !== "string") return false;
765
+ const normalized = input.normalize("NFKC");
727
766
  for (const pattern of PATH_PATTERNS) {
728
767
  pattern.lastIndex = 0;
729
- if (pattern.test(input)) {
768
+ if (pattern.test(normalized)) {
730
769
  return true;
731
770
  }
732
771
  }
@@ -784,7 +823,7 @@ function sanitizeString(value, options = {}) {
784
823
  if (value.length > maxSize) {
785
824
  throw new InputTooLargeError(maxSize, value.length);
786
825
  }
787
- const reject = options.mode !== "sanitize";
826
+ const reject = options.mode === "reject";
788
827
  let result = value;
789
828
  if (options.sql !== false) {
790
829
  if (reject) {
@@ -933,10 +972,22 @@ var SSTI_DETECT_PATTERNS = [
933
972
  /\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
934
973
  ];
935
974
  var SSTI_REMOVE_PATTERNS = [
975
+ /** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
936
976
  /\{\{.*?\}\}/g,
937
- /\$\{.*?\}/g,
977
+ /**
978
+ * Freemarker / Spring EL: ${...} — only strip when the expression contains
979
+ * operators (?!*+-/), method calls (), or known-dangerous prefixes.
980
+ * Bare ${name} and ${user.name} are left intact (JS template literal syntax).
981
+ */
982
+ /\$\{[^}]*[?!()*+\-/][^}]*\}/g,
983
+ /** ERB / EJS: <%= ... %> */
938
984
  /<%[=\-]?.*?%>/gs,
939
- /#\{.*?\}/g,
985
+ /**
986
+ * Pug / Jade: #{...} — same narrowing as ${ above.
987
+ * #{name} output expressions are left intact.
988
+ */
989
+ /#\{[^}]*[?!()*+\-/][^}]*\}/g,
990
+ /** Python dunder sandbox escape — always strip */
940
991
  /__(?:class|mro|subclasses|globals|builtins|import)__/gi
941
992
  ];
942
993
  function sanitizeSsti(input, collectThreats = false) {
@@ -1300,16 +1351,18 @@ function encodeForAttribute(value) {
1300
1351
  function encodeForJs(value) {
1301
1352
  if (!value) return "";
1302
1353
  let result = "";
1303
- for (let i = 0; i < value.length; i++) {
1304
- const ch = value.charCodeAt(i);
1305
- if (ch >= 48 && ch <= 57 || // 0-9
1306
- ch >= 65 && ch <= 90 || // A-Z
1307
- ch >= 97 && ch <= 122) {
1308
- result += value[i];
1309
- } else if (ch < 256) {
1310
- result += `\\x${ch.toString(16).toUpperCase().padStart(2, "0")}`;
1354
+ for (const char of value) {
1355
+ const cp = char.codePointAt(0);
1356
+ if (cp >= 48 && cp <= 57 || // 0-9
1357
+ cp >= 65 && cp <= 90 || // A-Z
1358
+ cp >= 97 && cp <= 122) {
1359
+ result += char;
1360
+ } else if (cp < 256) {
1361
+ result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
1362
+ } else if (cp <= 65535) {
1363
+ result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
1311
1364
  } else {
1312
- result += `\\u${ch.toString(16).toUpperCase().padStart(4, "0")}`;
1365
+ result += `\\u{${cp.toString(16).toUpperCase()}}`;
1313
1366
  }
1314
1367
  }
1315
1368
  return result;
@@ -1762,8 +1815,12 @@ function checkPrivateIp(hostname) {
1762
1815
  if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
1763
1816
  return "cloud metadata endpoint";
1764
1817
  }
1765
- const ipv6 = hostname.replace(/^\[|\]$/g, "");
1766
- if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
1818
+ let ipv6 = hostname.replace(/^\[|\]$/g, "");
1819
+ const zoneIdx = ipv6.indexOf("%");
1820
+ if (zoneIdx !== -1) {
1821
+ ipv6 = ipv6.slice(0, zoneIdx);
1822
+ }
1823
+ if (ipv6 === "::1" || ipv6 === "::" || /^fc[0-9a-f]{2}:/i.test(ipv6) || /^fd[0-9a-f]{2}:/i.test(ipv6) || /^fe80:/i.test(ipv6) || /^ff[0-9a-f]{2}:/i.test(ipv6)) {
1767
1824
  return "private IPv6 address";
1768
1825
  }
1769
1826
  const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
@@ -2822,11 +2879,9 @@ function generateCsrfToken(length = 32) {
2822
2879
  function validateCsrfToken(cookieToken, requestToken) {
2823
2880
  if (!cookieToken || !requestToken) return false;
2824
2881
  if (cookieToken.length !== requestToken.length) return false;
2825
- let result = 0;
2826
- for (let i = 0; i < cookieToken.length; i++) {
2827
- result |= cookieToken.charCodeAt(i) ^ requestToken.charCodeAt(i);
2828
- }
2829
- return result === 0;
2882
+ const a = Buffer.from(cookieToken);
2883
+ const b = Buffer.from(requestToken);
2884
+ return crypto.timingSafeEqual(a, b);
2830
2885
  }
2831
2886
  function getRequestToken(req, headerName, fieldName) {
2832
2887
  const headerToken = req.headers[headerName.toLowerCase()];
@@ -2835,10 +2890,6 @@ function getRequestToken(req, headerName, fieldName) {
2835
2890
  const bodyToken = req.body[fieldName];
2836
2891
  if (typeof bodyToken === "string" && bodyToken) return bodyToken;
2837
2892
  }
2838
- if (req.query && fieldName in req.query) {
2839
- const queryToken = req.query[fieldName];
2840
- if (typeof queryToken === "string" && queryToken) return queryToken;
2841
- }
2842
2893
  return void 0;
2843
2894
  }
2844
2895
  function csrfProtection(options = {}) {
@@ -2921,7 +2972,15 @@ function setCsrfCookie(res, name, token, opts) {
2921
2972
  if (opts.secure) parts.push("Secure");
2922
2973
  parts.push(`SameSite=${opts.sameSite}`);
2923
2974
  if (opts.domain) parts.push(`Domain=${opts.domain}`);
2924
- res.setHeader("Set-Cookie", parts.join("; "));
2975
+ const newCookie = parts.join("; ");
2976
+ const existing = res.getHeader("Set-Cookie");
2977
+ if (existing === void 0) {
2978
+ res.setHeader("Set-Cookie", newCookie);
2979
+ } else if (Array.isArray(existing)) {
2980
+ res.setHeader("Set-Cookie", [...existing, newCookie]);
2981
+ } else {
2982
+ res.setHeader("Set-Cookie", [existing, newCookie]);
2983
+ }
2925
2984
  }
2926
2985
  function escapeRegex(str) {
2927
2986
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -3105,8 +3164,9 @@ function fingerprint(req, options = {}) {
3105
3164
  }
3106
3165
 
3107
3166
  // src/stores/memory.ts
3167
+ var DEFAULT_MAX_SIZE2 = 1e4;
3108
3168
  var MemoryStore = class {
3109
- constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
3169
+ constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE2) {
3110
3170
  this.store = /* @__PURE__ */ new Map();
3111
3171
  this.cleanupInterval = null;
3112
3172
  if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
@@ -3114,7 +3174,11 @@ var MemoryStore = class {
3114
3174
  `MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
3115
3175
  );
3116
3176
  }
3177
+ if (!Number.isFinite(maxSize) || maxSize < 1) {
3178
+ throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
3179
+ }
3117
3180
  this.windowMs = windowMs;
3181
+ this.maxSize = maxSize;
3118
3182
  this.startCleanup();
3119
3183
  }
3120
3184
  /**
@@ -3146,18 +3210,33 @@ var MemoryStore = class {
3146
3210
  return entry;
3147
3211
  }
3148
3212
  async set(key, entry) {
3213
+ if (!this.store.has(key) && this.store.size >= this.maxSize) {
3214
+ this.evictExpired();
3215
+ if (this.store.size >= this.maxSize) return;
3216
+ }
3149
3217
  this.store.set(key, entry);
3150
3218
  }
3151
3219
  async increment(key) {
3152
3220
  const now = Date.now();
3153
3221
  const entry = this.store.get(key);
3154
3222
  if (!entry || entry.resetTime < now) {
3223
+ if (this.store.size >= this.maxSize) {
3224
+ this.evictExpired();
3225
+ if (this.store.size >= this.maxSize) return 1;
3226
+ }
3155
3227
  this.store.set(key, { count: 1, resetTime: now + this.windowMs });
3156
3228
  return 1;
3157
3229
  }
3158
3230
  entry.count++;
3159
3231
  return entry.count;
3160
3232
  }
3233
+ /** Eagerly remove expired entries to reclaim capacity. */
3234
+ evictExpired() {
3235
+ const now = Date.now();
3236
+ for (const [key, entry] of this.store.entries()) {
3237
+ if (entry.resetTime < now) this.store.delete(key);
3238
+ }
3239
+ }
3161
3240
  async decrement(key) {
3162
3241
  const entry = this.store.get(key);
3163
3242
  if (entry && entry.count > 0) {