@arcis/node 1.1.0 → 1.2.0

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 (50) hide show
  1. package/README.md +156 -211
  2. package/dist/core/index.d.mts +4 -4
  3. package/dist/core/index.d.ts +4 -4
  4. package/dist/core/index.js +13 -2
  5. package/dist/core/index.js.map +1 -1
  6. package/dist/core/index.mjs +13 -2
  7. package/dist/core/index.mjs.map +1 -1
  8. package/dist/{index-CslcoZUN.d.mts → index-A-m-pPeW.d.mts} +1 -1
  9. package/dist/{index-CCcPuTBo.d.mts → index-CgK94hY_.d.mts} +96 -2
  10. package/dist/{index-iCOw8Fcg.d.ts → index-Co5kPRZz.d.ts} +1 -1
  11. package/dist/{index-BvcFpoR3.d.ts → index-D_bdJcF0.d.ts} +96 -2
  12. package/dist/index.d.mts +4 -4
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +553 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +540 -7
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/logging/index.d.mts +1 -1
  19. package/dist/logging/index.d.ts +1 -1
  20. package/dist/logging/index.js +12 -1
  21. package/dist/logging/index.js.map +1 -1
  22. package/dist/logging/index.mjs +12 -1
  23. package/dist/logging/index.mjs.map +1 -1
  24. package/dist/middleware/index.d.mts +2 -2
  25. package/dist/middleware/index.d.ts +2 -2
  26. package/dist/middleware/index.js +146 -4
  27. package/dist/middleware/index.js.map +1 -1
  28. package/dist/middleware/index.mjs +143 -5
  29. package/dist/middleware/index.mjs.map +1 -1
  30. package/dist/{headers-DBQedhrb.d.mts → pii-CXcHMlnX.d.mts} +156 -2
  31. package/dist/{headers-BJq2OA0i.d.ts → pii-DhNpl7M3.d.ts} +156 -2
  32. package/dist/sanitizers/index.d.mts +2 -2
  33. package/dist/sanitizers/index.d.ts +2 -2
  34. package/dist/sanitizers/index.js +331 -3
  35. package/dist/sanitizers/index.js.map +1 -1
  36. package/dist/sanitizers/index.mjs +321 -4
  37. package/dist/sanitizers/index.mjs.map +1 -1
  38. package/dist/stores/index.d.mts +1 -1
  39. package/dist/stores/index.d.ts +1 -1
  40. package/dist/stores/index.js.map +1 -1
  41. package/dist/stores/index.mjs.map +1 -1
  42. package/dist/{types-BOdL3ZWo.d.mts → types-CsOFHoD9.d.mts} +6 -1
  43. package/dist/{types-BOdL3ZWo.d.ts → types-CsOFHoD9.d.ts} +6 -1
  44. package/dist/validation/index.d.mts +2 -2
  45. package/dist/validation/index.d.ts +2 -2
  46. package/dist/validation/index.js +105 -3
  47. package/dist/validation/index.js.map +1 -1
  48. package/dist/validation/index.mjs +105 -3
  49. package/dist/validation/index.mjs.map +1 -1
  50. package/package.json +114 -114
package/dist/index.js CHANGED
@@ -119,7 +119,11 @@ var SQL_PATTERNS = [
119
119
  /** Time-based blind: SLEEP() */
120
120
  /\bSLEEP\s*\(\s*\d+\s*\)/gi,
121
121
  /** Time-based blind: BENCHMARK() */
122
- /\bBENCHMARK\s*\(/gi
122
+ /\bBENCHMARK\s*\(/gi,
123
+ /** Time-based blind: PostgreSQL pg_sleep() */
124
+ /\bpg_sleep\s*\(/gi,
125
+ /** Time-based blind: MSSQL WAITFOR DELAY */
126
+ /\bWAITFOR\s+DELAY\b/gi
123
127
  ];
124
128
  var PATH_PATTERNS = [
125
129
  /** Unix path traversal */
@@ -137,6 +141,10 @@ var PATH_PATTERNS = [
137
141
  /\.%2e[\\/]/gi,
138
142
  /** Fully URL-encoded: %2e%2e%2f */
139
143
  /%2e%2e%2f/gi,
144
+ /** Double URL-encoded forward slash: %252f */
145
+ /%252f/gi,
146
+ /** Dotdotslash bypass: ....// or ....\\ */
147
+ /\.{2,}[/\\]{2,}/g,
140
148
  /** Null byte injection in paths */
141
149
  /\0/g
142
150
  ];
@@ -152,7 +160,9 @@ var COMMAND_PATTERNS = [
152
160
  */
153
161
  /[;&|`]/g,
154
162
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
155
- /\$\(/g
163
+ /\$\(/g,
164
+ /** URL-encoded newline/carriage-return injection (%0a, %0d) */
165
+ /%0[ad]/gi
156
166
  ];
157
167
  var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
158
168
  "__proto__",
@@ -186,6 +196,7 @@ var NOSQL_DANGEROUS_KEYS = /* @__PURE__ */ new Set([
186
196
  "$expr",
187
197
  "$mod",
188
198
  "$text",
199
+ "$jsonSchema",
189
200
  // Array
190
201
  "$elemMatch",
191
202
  "$all",
@@ -786,7 +797,8 @@ function sanitizeObject(obj, options = {}) {
786
797
  if (typeof obj === "string") return sanitizeString(obj, options);
787
798
  if (typeof obj !== "object") return obj;
788
799
  if (Array.isArray(obj)) return obj.map((item) => sanitizeObject(item, options));
789
- return sanitizeObjectDepth(obj, options, 0);
800
+ const result = sanitizeObjectDepth(obj, options, 0);
801
+ return options.freeze ? Object.freeze(result) : result;
790
802
  }
791
803
  function sanitizeObjectDepth(obj, options, depth) {
792
804
  if (depth >= INPUT.MAX_RECURSION_DEPTH) return obj;
@@ -883,6 +895,179 @@ function detectPrototypePollution(obj, maxDepth = 10) {
883
895
  return false;
884
896
  }
885
897
 
898
+ // src/sanitizers/ssti.ts
899
+ var SSTI_DETECT_PATTERNS = [
900
+ /** Jinja2 / Twig / Nunjucks: {{ ... }} */
901
+ /\{\{.*?\}\}/g,
902
+ /** Freemarker / Thymeleaf / Spring EL: ${ ... } */
903
+ /\$\{.*?\}/g,
904
+ /** ERB / EJS: <%= ... %> or <% ... %> */
905
+ /<%[=\-]?.*?%>/gs,
906
+ /** Pug / Jade / Slim: #{ ... } */
907
+ /#\{.*?\}/g,
908
+ /** Python dunder sandbox escape */
909
+ /__(?:class|mro|subclasses|globals|builtins|import)__/gi,
910
+ /** Jinja2 config leak: {{config.X}} or {{config['X']}} */
911
+ /\{\{\s*config[.\[]/gi,
912
+ /** Jinja2 built-in objects */
913
+ /\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
914
+ ];
915
+ var SSTI_REMOVE_PATTERNS = [
916
+ /\{\{.*?\}\}/g,
917
+ /\$\{.*?\}/g,
918
+ /<%[=\-]?.*?%>/gs,
919
+ /#\{.*?\}/g,
920
+ /__(?:class|mro|subclasses|globals|builtins|import)__/gi
921
+ ];
922
+ function sanitizeSsti(input, collectThreats = false) {
923
+ if (typeof input !== "string") {
924
+ return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
925
+ }
926
+ const threats = [];
927
+ let value = input;
928
+ let wasSanitized = false;
929
+ for (const pattern of SSTI_REMOVE_PATTERNS) {
930
+ pattern.lastIndex = 0;
931
+ if (pattern.test(value)) {
932
+ pattern.lastIndex = 0;
933
+ if (collectThreats) {
934
+ const matches = value.match(pattern);
935
+ if (matches) {
936
+ for (const match of matches) {
937
+ threats.push({
938
+ type: "ssti",
939
+ pattern: pattern.source,
940
+ original: match
941
+ });
942
+ }
943
+ }
944
+ }
945
+ value = value.replace(pattern, "");
946
+ wasSanitized = true;
947
+ }
948
+ }
949
+ if (collectThreats) {
950
+ return { value, wasSanitized, threats };
951
+ }
952
+ return value;
953
+ }
954
+ function detectSsti(input) {
955
+ if (typeof input !== "string") return false;
956
+ for (const pattern of SSTI_DETECT_PATTERNS) {
957
+ pattern.lastIndex = 0;
958
+ if (pattern.test(input)) {
959
+ return true;
960
+ }
961
+ }
962
+ return false;
963
+ }
964
+
965
+ // src/sanitizers/xxe.ts
966
+ var XXE_DETECT_PATTERNS = [
967
+ /** DOCTYPE declaration */
968
+ /<!DOCTYPE\b/gi,
969
+ /** ENTITY declaration */
970
+ /<!ENTITY\b/gi,
971
+ /** SYSTEM keyword with URI */
972
+ /\bSYSTEM\s+["']/gi,
973
+ /** PUBLIC keyword with URI */
974
+ /\bPUBLIC\s+["']/gi,
975
+ /** Parameter entity reference (%entity;) */
976
+ /%\s*\w+\s*;/g,
977
+ /** CDATA section (often used to smuggle payloads) */
978
+ /<!\[CDATA\[/gi
979
+ ];
980
+ var XXE_REMOVE_PATTERNS = [
981
+ /** Full DOCTYPE block with optional internal subset: <!DOCTYPE ... [...]> */
982
+ /<!DOCTYPE\s[^[>]*(?:\[[^\]]*\]\s*)?>|<!DOCTYPE\s[^>]*>/gi,
983
+ /** Full ENTITY declaration: <!ENTITY ... > */
984
+ /<!ENTITY[^>]*>/gi,
985
+ /** CDATA sections: <![CDATA[ ... ]]> */
986
+ /<!\[CDATA\[[\s\S]*?\]\]>/gi
987
+ ];
988
+ function sanitizeXxe(input, collectThreats = false) {
989
+ if (typeof input !== "string") {
990
+ return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
991
+ }
992
+ const threats = [];
993
+ let value = input;
994
+ let wasSanitized = false;
995
+ for (const pattern of XXE_REMOVE_PATTERNS) {
996
+ pattern.lastIndex = 0;
997
+ if (pattern.test(value)) {
998
+ pattern.lastIndex = 0;
999
+ if (collectThreats) {
1000
+ const matches = value.match(pattern);
1001
+ if (matches) {
1002
+ for (const match of matches) {
1003
+ threats.push({
1004
+ type: "xxe",
1005
+ pattern: pattern.source,
1006
+ original: match
1007
+ });
1008
+ }
1009
+ }
1010
+ }
1011
+ value = value.replace(pattern, "");
1012
+ wasSanitized = true;
1013
+ }
1014
+ }
1015
+ if (collectThreats) {
1016
+ return { value, wasSanitized, threats };
1017
+ }
1018
+ return value;
1019
+ }
1020
+ function detectXxe(input) {
1021
+ if (typeof input !== "string") return false;
1022
+ for (const pattern of XXE_DETECT_PATTERNS) {
1023
+ pattern.lastIndex = 0;
1024
+ if (pattern.test(input)) {
1025
+ return true;
1026
+ }
1027
+ }
1028
+ return false;
1029
+ }
1030
+
1031
+ // src/sanitizers/jsonp.ts
1032
+ var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.[\]]*$/;
1033
+ var DANGEROUS_CALLBACK_PATTERNS = [
1034
+ /\.\./,
1035
+ // prototype chain traversal
1036
+ /\[\s*\]/
1037
+ // empty bracket access
1038
+ ];
1039
+ function sanitizeJsonpCallback(callback, maxLength = 128) {
1040
+ if (typeof callback !== "string" || callback.length === 0) {
1041
+ return null;
1042
+ }
1043
+ if (callback.length > maxLength) {
1044
+ return null;
1045
+ }
1046
+ if (!SAFE_CALLBACK_PATTERN.test(callback)) {
1047
+ return null;
1048
+ }
1049
+ for (const pattern of DANGEROUS_CALLBACK_PATTERNS) {
1050
+ if (pattern.test(callback)) {
1051
+ return null;
1052
+ }
1053
+ }
1054
+ return callback;
1055
+ }
1056
+ function detectJsonpInjection(callback) {
1057
+ if (typeof callback !== "string" || callback.length === 0) {
1058
+ return false;
1059
+ }
1060
+ if (!SAFE_CALLBACK_PATTERN.test(callback)) {
1061
+ return true;
1062
+ }
1063
+ for (const pattern of DANGEROUS_CALLBACK_PATTERNS) {
1064
+ if (pattern.test(callback)) {
1065
+ return true;
1066
+ }
1067
+ }
1068
+ return false;
1069
+ }
1070
+
886
1071
  // src/sanitizers/headers.ts
887
1072
  var HEADER_INJECTION_PATTERN = /\r\n|\r|\n|\0/g;
888
1073
  function sanitizeHeaderValue(input, collectThreats = false) {
@@ -932,6 +1117,138 @@ function detectHeaderInjection(input) {
932
1117
  return HEADER_INJECTION_PATTERN.test(input);
933
1118
  }
934
1119
 
1120
+ // src/sanitizers/pii.ts
1121
+ var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+/g;
1122
+ var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
1123
+ var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
1124
+ var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
1125
+ var IPV4_RE = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g;
1126
+ var IPV6_RE = /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:|::(?:[0-9a-fA-F]{1,4}:){0,5}[0-9a-fA-F]{1,4}\b/g;
1127
+ var PATTERN_MAP = {
1128
+ email: [EMAIL_RE],
1129
+ phone: [PHONE_RE],
1130
+ credit_card: [CREDIT_CARD_RE],
1131
+ ssn: [SSN_RE],
1132
+ ip_address: [IPV4_RE, IPV6_RE]
1133
+ };
1134
+ var ALL_TYPES = ["email", "phone", "credit_card", "ssn", "ip_address"];
1135
+ var TYPE_LABELS = {
1136
+ email: "[EMAIL]",
1137
+ phone: "[PHONE]",
1138
+ credit_card: "[CREDIT_CARD]",
1139
+ ssn: "[SSN]",
1140
+ ip_address: "[IP_ADDRESS]"
1141
+ };
1142
+ function luhnCheck(value) {
1143
+ const digits = value.replace(/[\s-]/g, "");
1144
+ if (!/^\d{13,19}$/.test(digits)) return false;
1145
+ let sum = 0;
1146
+ let alternate = false;
1147
+ for (let i = digits.length - 1; i >= 0; i--) {
1148
+ let n = parseInt(digits[i], 10);
1149
+ if (alternate) {
1150
+ n *= 2;
1151
+ if (n > 9) n -= 9;
1152
+ }
1153
+ sum += n;
1154
+ alternate = !alternate;
1155
+ }
1156
+ return sum % 10 === 0;
1157
+ }
1158
+ function scanPii(input, options = {}) {
1159
+ if (!input || typeof input !== "string") return [];
1160
+ const types = options.types ?? ALL_TYPES;
1161
+ const matches = [];
1162
+ for (const type of types) {
1163
+ const patterns = PATTERN_MAP[type];
1164
+ if (!patterns) continue;
1165
+ for (const pattern of patterns) {
1166
+ const re = new RegExp(pattern.source, pattern.flags);
1167
+ let match;
1168
+ while ((match = re.exec(input)) !== null) {
1169
+ const value = match[0];
1170
+ if (type === "credit_card" && !luhnCheck(value)) continue;
1171
+ if (type === "ssn") {
1172
+ const area = parseInt(value.substring(0, 3), 10);
1173
+ if (area === 0 || area === 666 || area >= 900) continue;
1174
+ }
1175
+ matches.push({
1176
+ type,
1177
+ value,
1178
+ start: match.index,
1179
+ end: match.index + value.length
1180
+ });
1181
+ }
1182
+ }
1183
+ }
1184
+ matches.sort((a, b) => a.start - b.start);
1185
+ return matches;
1186
+ }
1187
+ function detectPii(input, options = {}) {
1188
+ return scanPii(input, options).length > 0;
1189
+ }
1190
+ function redactPii(input, options = {}) {
1191
+ if (!input || typeof input !== "string") return input;
1192
+ const matches = scanPii(input, options);
1193
+ if (matches.length === 0) return input;
1194
+ const replacement = options.replacement ?? "[REDACTED]";
1195
+ let result = input;
1196
+ for (let i = matches.length - 1; i >= 0; i--) {
1197
+ const m = matches[i];
1198
+ const label = options.typeLabels ? TYPE_LABELS[m.type] : replacement;
1199
+ result = result.substring(0, m.start) + label + result.substring(m.end);
1200
+ }
1201
+ return result;
1202
+ }
1203
+ function scanObjectPii(obj, options = {}, path = "") {
1204
+ const results = [];
1205
+ if (!obj || typeof obj !== "object") return results;
1206
+ for (const [key, value] of Object.entries(obj)) {
1207
+ const fieldPath = path ? `${path}.${key}` : key;
1208
+ if (typeof value === "string") {
1209
+ const matches = scanPii(value, options);
1210
+ for (const m of matches) {
1211
+ results.push({ ...m, field: fieldPath });
1212
+ }
1213
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
1214
+ results.push(...scanObjectPii(value, options, fieldPath));
1215
+ } else if (Array.isArray(value)) {
1216
+ for (let i = 0; i < value.length; i++) {
1217
+ const item = value[i];
1218
+ if (typeof item === "string") {
1219
+ const matches = scanPii(item, options);
1220
+ for (const m of matches) {
1221
+ results.push({ ...m, field: `${fieldPath}[${i}]` });
1222
+ }
1223
+ } else if (item && typeof item === "object") {
1224
+ results.push(...scanObjectPii(item, options, `${fieldPath}[${i}]`));
1225
+ }
1226
+ }
1227
+ }
1228
+ }
1229
+ return results;
1230
+ }
1231
+ function redactObjectPii(obj, options = {}) {
1232
+ if (!obj || typeof obj !== "object") return obj;
1233
+ const result = {};
1234
+ for (const [key, value] of Object.entries(obj)) {
1235
+ if (typeof value === "string") {
1236
+ result[key] = redactPii(value, options);
1237
+ } else if (Array.isArray(value)) {
1238
+ result[key] = value.map((item) => {
1239
+ if (typeof item === "string") return redactPii(item, options);
1240
+ if (item && typeof item === "object") return redactObjectPii(item, options);
1241
+ return item;
1242
+ });
1243
+ } else if (value && typeof value === "object") {
1244
+ result[key] = redactObjectPii(value, options);
1245
+ } else {
1246
+ result[key] = value;
1247
+ }
1248
+ }
1249
+ return result;
1250
+ }
1251
+
935
1252
  // src/validation/schema.ts
936
1253
  function validate(schema, source = "body") {
937
1254
  return (req, res, next) => {
@@ -1312,6 +1629,18 @@ function validateUrl(url, options = {}) {
1312
1629
  return { safe: false, reason: "loopback address" };
1313
1630
  }
1314
1631
  }
1632
+ if (!allowLocalhost || !allowPrivate) {
1633
+ const decimalCheck = checkDecimalIp(hostname, allowLocalhost, allowPrivate);
1634
+ if (decimalCheck) {
1635
+ return { safe: false, reason: decimalCheck };
1636
+ }
1637
+ }
1638
+ if (!allowLocalhost || !allowPrivate) {
1639
+ const octalCheck = checkOctalIp(hostname, allowLocalhost, allowPrivate);
1640
+ if (octalCheck) {
1641
+ return { safe: false, reason: octalCheck };
1642
+ }
1643
+ }
1315
1644
  if (!allowPrivate) {
1316
1645
  const privateCheck = checkPrivateIp(hostname);
1317
1646
  if (privateCheck) {
@@ -1343,13 +1672,93 @@ function checkPrivateIp(hostname) {
1343
1672
  if (/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) {
1344
1673
  return "current network address (0.0.0.0/8)";
1345
1674
  }
1346
- if (hostname === "metadata.google.internal" || hostname === "metadata.internal") {
1675
+ if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
1347
1676
  return "cloud metadata endpoint";
1348
1677
  }
1349
1678
  const ipv6 = hostname.replace(/^\[|\]$/g, "");
1350
1679
  if (ipv6 === "::1" || ipv6 === "::" || ipv6.startsWith("fc") || ipv6.startsWith("fd") || ipv6.startsWith("fe80")) {
1351
1680
  return "private IPv6 address";
1352
1681
  }
1682
+ const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
1683
+ if (mappedDotted) {
1684
+ const mappedIp = mappedDotted[1];
1685
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(mappedIp)) {
1686
+ return "IPv6-mapped loopback address";
1687
+ }
1688
+ const mappedCheck = checkPrivateIp(mappedIp);
1689
+ if (mappedCheck) {
1690
+ return `IPv6-mapped ${mappedCheck}`;
1691
+ }
1692
+ }
1693
+ const mappedHex = ipv6.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
1694
+ if (mappedHex) {
1695
+ const hi = parseInt(mappedHex[1], 16);
1696
+ const lo = parseInt(mappedHex[2], 16);
1697
+ const a = hi >> 8 & 255;
1698
+ const b = hi & 255;
1699
+ const c = lo >> 8 & 255;
1700
+ const d = lo & 255;
1701
+ const dotted = `${a}.${b}.${c}.${d}`;
1702
+ if (a === 127) {
1703
+ return "IPv6-mapped loopback address";
1704
+ }
1705
+ const hexCheck = checkPrivateIp(dotted);
1706
+ if (hexCheck) {
1707
+ return `IPv6-mapped ${hexCheck}`;
1708
+ }
1709
+ }
1710
+ return null;
1711
+ }
1712
+ function checkDecimalIp(hostname, allowLocalhost, allowPrivate) {
1713
+ if (!/^\d+$/.test(hostname)) return null;
1714
+ const num = parseInt(hostname, 10);
1715
+ if (isNaN(num) || num < 0 || num > 4294967295) return null;
1716
+ const a = num >>> 24 & 255;
1717
+ const b = num >>> 16 & 255;
1718
+ const c = num >>> 8 & 255;
1719
+ const d = num & 255;
1720
+ const dotted = `${a}.${b}.${c}.${d}`;
1721
+ if (!allowLocalhost && a === 127) {
1722
+ return `loopback address (decimal IP: ${dotted})`;
1723
+ }
1724
+ if (!allowPrivate) {
1725
+ const privateCheck = checkPrivateIp(dotted);
1726
+ if (privateCheck) {
1727
+ return `${privateCheck} (decimal IP: ${dotted})`;
1728
+ }
1729
+ }
1730
+ return null;
1731
+ }
1732
+ function checkOctalIp(hostname, allowLocalhost, allowPrivate) {
1733
+ const parts = hostname.split(".");
1734
+ if (parts.length !== 4) return null;
1735
+ const hasAlternateNotation = parts.some((p) => /^0[0-7]+$/.test(p) || /^0x[0-9a-fA-F]+$/i.test(p));
1736
+ if (!hasAlternateNotation) return null;
1737
+ const octets = [];
1738
+ for (const part of parts) {
1739
+ let val;
1740
+ if (/^0x[0-9a-fA-F]+$/i.test(part)) {
1741
+ val = parseInt(part, 16);
1742
+ } else if (/^0[0-7]*$/.test(part)) {
1743
+ val = parseInt(part, 8);
1744
+ } else if (/^\d+$/.test(part)) {
1745
+ val = parseInt(part, 10);
1746
+ } else {
1747
+ return null;
1748
+ }
1749
+ if (val < 0 || val > 255) return null;
1750
+ octets.push(val);
1751
+ }
1752
+ const dotted = octets.join(".");
1753
+ if (!allowLocalhost && octets[0] === 127) {
1754
+ return `loopback address (octal IP: ${dotted})`;
1755
+ }
1756
+ if (!allowPrivate) {
1757
+ const privateCheck = checkPrivateIp(dotted);
1758
+ if (privateCheck) {
1759
+ return `${privateCheck} (octal IP: ${dotted})`;
1760
+ }
1761
+ }
1353
1762
  return null;
1354
1763
  }
1355
1764
 
@@ -1665,12 +2074,21 @@ function isValidEmailSyntax(email) {
1665
2074
  }
1666
2075
 
1667
2076
  // src/logging/redactor.ts
2077
+ var LOG_LEVELS = {
2078
+ debug: 0,
2079
+ info: 1,
2080
+ warn: 2,
2081
+ error: 3,
2082
+ silent: 4
2083
+ };
1668
2084
  function createSafeLogger(options = {}) {
1669
2085
  const {
1670
2086
  redactKeys = [],
1671
2087
  maxLength = REDACTION.DEFAULT_MAX_LENGTH,
1672
- redactPatterns = []
2088
+ redactPatterns = [],
2089
+ level: minLevel = "debug"
1673
2090
  } = options;
2091
+ const minLevelNum = LOG_LEVELS[minLevel] ?? 0;
1674
2092
  const allRedactKeys = /* @__PURE__ */ new Set([
1675
2093
  ...Array.from(REDACTION.SENSITIVE_KEYS),
1676
2094
  ...redactKeys.map((k) => k.toLowerCase())
@@ -1696,6 +2114,8 @@ function createSafeLogger(options = {}) {
1696
2114
  return result;
1697
2115
  }
1698
2116
  function log(level, message, data) {
2117
+ const levelNum = LOG_LEVELS[level] ?? 0;
2118
+ if (levelNum < minLevelNum) return;
1699
2119
  const entry = {
1700
2120
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1701
2121
  level,
@@ -2302,6 +2722,119 @@ function botProtection(options = {}) {
2302
2722
  next();
2303
2723
  };
2304
2724
  }
2725
+ var DEFAULTS = {
2726
+ cookieName: "_csrf",
2727
+ headerName: "x-csrf-token",
2728
+ fieldName: "_csrf",
2729
+ tokenLength: 32,
2730
+ protectedMethods: ["POST", "PUT", "PATCH", "DELETE"]
2731
+ };
2732
+ function generateCsrfToken(length = 32) {
2733
+ return crypto.randomBytes(length).toString("hex");
2734
+ }
2735
+ function validateCsrfToken(cookieToken, requestToken) {
2736
+ if (!cookieToken || !requestToken) return false;
2737
+ if (cookieToken.length !== requestToken.length) return false;
2738
+ let result = 0;
2739
+ for (let i = 0; i < cookieToken.length; i++) {
2740
+ result |= cookieToken.charCodeAt(i) ^ requestToken.charCodeAt(i);
2741
+ }
2742
+ return result === 0;
2743
+ }
2744
+ function getRequestToken(req, headerName, fieldName) {
2745
+ const headerToken = req.headers[headerName.toLowerCase()];
2746
+ if (typeof headerToken === "string" && headerToken) return headerToken;
2747
+ if (req.body && typeof req.body === "object" && fieldName in req.body) {
2748
+ const bodyToken = req.body[fieldName];
2749
+ if (typeof bodyToken === "string" && bodyToken) return bodyToken;
2750
+ }
2751
+ if (req.query && fieldName in req.query) {
2752
+ const queryToken = req.query[fieldName];
2753
+ if (typeof queryToken === "string" && queryToken) return queryToken;
2754
+ }
2755
+ return void 0;
2756
+ }
2757
+ function csrfProtection(options = {}) {
2758
+ const cookieName = options.cookieName ?? DEFAULTS.cookieName;
2759
+ const headerName = options.headerName ?? DEFAULTS.headerName;
2760
+ const fieldName = options.fieldName ?? DEFAULTS.fieldName;
2761
+ const tokenLength = options.tokenLength ?? DEFAULTS.tokenLength;
2762
+ const protectedMethods = options.protectedMethods ?? [...DEFAULTS.protectedMethods];
2763
+ const excludePaths = options.excludePaths ?? [];
2764
+ const isProduction = process.env.NODE_ENV === "production";
2765
+ const cookieOpts = {
2766
+ path: options.cookie?.path ?? "/",
2767
+ httpOnly: options.cookie?.httpOnly ?? false,
2768
+ // Must be readable by client JS
2769
+ secure: options.cookie?.secure ?? isProduction,
2770
+ sameSite: options.cookie?.sameSite ?? "Lax",
2771
+ domain: options.cookie?.domain
2772
+ };
2773
+ const defaultOnError = (_req, res, _next) => {
2774
+ res.status(403).json({
2775
+ error: "CSRF token validation failed",
2776
+ message: "Invalid or missing CSRF token. Include the token from the cookie in the X-CSRF-Token header."
2777
+ });
2778
+ };
2779
+ const onError = options.onError ?? defaultOnError;
2780
+ const protectedSet = new Set(protectedMethods.map((m) => m.toUpperCase()));
2781
+ return (req, res, next) => {
2782
+ const method = req.method.toUpperCase();
2783
+ const requestPath = req.path || req.url;
2784
+ if (excludePaths.some((p) => requestPath === p || requestPath.startsWith(p + "/"))) {
2785
+ return next();
2786
+ }
2787
+ req.csrfToken = () => {
2788
+ const existing = getCookieValue(req, cookieName);
2789
+ if (existing) return existing;
2790
+ const token = generateCsrfToken(tokenLength);
2791
+ setCsrfCookie(res, cookieName, token, cookieOpts);
2792
+ return token;
2793
+ };
2794
+ if (!protectedSet.has(method)) {
2795
+ const existing = getCookieValue(req, cookieName);
2796
+ if (!existing) {
2797
+ const token = generateCsrfToken(tokenLength);
2798
+ setCsrfCookie(res, cookieName, token, cookieOpts);
2799
+ }
2800
+ return next();
2801
+ }
2802
+ const cookieToken = getCookieValue(req, cookieName);
2803
+ if (!cookieToken) {
2804
+ return onError(req, res, next);
2805
+ }
2806
+ const requestToken = getRequestToken(req, headerName, fieldName);
2807
+ if (!requestToken) {
2808
+ return onError(req, res, next);
2809
+ }
2810
+ if (!validateCsrfToken(cookieToken, requestToken)) {
2811
+ return onError(req, res, next);
2812
+ }
2813
+ next();
2814
+ };
2815
+ }
2816
+ function getCookieValue(req, name) {
2817
+ if (req.cookies && typeof req.cookies === "object" && name in req.cookies) {
2818
+ return req.cookies[name];
2819
+ }
2820
+ const cookieHeader = req.headers.cookie;
2821
+ if (!cookieHeader) return void 0;
2822
+ const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${escapeRegex(name)}=([^;]*)`));
2823
+ return match ? decodeURIComponent(match[1]) : void 0;
2824
+ }
2825
+ function setCsrfCookie(res, name, token, opts) {
2826
+ const parts = [`${name}=${token}`];
2827
+ parts.push(`Path=${opts.path}`);
2828
+ if (opts.httpOnly) parts.push("HttpOnly");
2829
+ if (opts.secure) parts.push("Secure");
2830
+ parts.push(`SameSite=${opts.sameSite}`);
2831
+ if (opts.domain) parts.push(`Domain=${opts.domain}`);
2832
+ res.setHeader("Set-Cookie", parts.join("; "));
2833
+ }
2834
+ function escapeRegex(str) {
2835
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2836
+ }
2837
+ var createCsrf = csrfProtection;
2305
2838
 
2306
2839
  // src/utils/ip.ts
2307
2840
  var PLATFORM_HEADERS = {
@@ -2585,6 +3118,7 @@ exports.arcis = arcis;
2585
3118
  exports.arcisFunction = arcisWithMethods;
2586
3119
  exports.botProtection = botProtection;
2587
3120
  exports.createCors = createCors;
3121
+ exports.createCsrf = createCsrf;
2588
3122
  exports.createErrorHandler = createErrorHandler;
2589
3123
  exports.createHeaders = createHeaders;
2590
3124
  exports.createRateLimiter = createRateLimiter;
@@ -2596,20 +3130,26 @@ exports.createSecureCookies = createSecureCookies;
2596
3130
  exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
2597
3131
  exports.createTokenBucketLimiter = createTokenBucketLimiter;
2598
3132
  exports.createValidator = createValidator;
3133
+ exports.csrfProtection = csrfProtection;
2599
3134
  exports.default = main_default;
2600
3135
  exports.detectBot = detectBot;
2601
3136
  exports.detectClientIp = detectClientIp;
2602
3137
  exports.detectCommandInjection = detectCommandInjection;
2603
3138
  exports.detectHeaderInjection = detectHeaderInjection;
3139
+ exports.detectJsonpInjection = detectJsonpInjection;
2604
3140
  exports.detectNoSqlInjection = detectNoSqlInjection;
2605
3141
  exports.detectPathTraversal = detectPathTraversal;
3142
+ exports.detectPii = detectPii;
2606
3143
  exports.detectPrototypePollution = detectPrototypePollution;
2607
3144
  exports.detectSql = detectSql;
3145
+ exports.detectSsti = detectSsti;
2608
3146
  exports.detectXss = detectXss;
3147
+ exports.detectXxe = detectXxe;
2609
3148
  exports.enforceSecureCookie = enforceSecureCookie;
2610
3149
  exports.errorHandler = errorHandler;
2611
3150
  exports.fingerprint = fingerprint;
2612
3151
  exports.formatDuration = formatDuration;
3152
+ exports.generateCsrfToken = generateCsrfToken;
2613
3153
  exports.isDangerousExtension = isDangerousExtension;
2614
3154
  exports.isDangerousNoSqlKey = isDangerousNoSqlKey;
2615
3155
  exports.isDangerousProtoKey = isDangerousProtoKey;
@@ -2619,20 +3159,28 @@ exports.isUrlSafe = isUrlSafe;
2619
3159
  exports.isValidEmailSyntax = isValidEmailSyntax;
2620
3160
  exports.parseDuration = parseDuration;
2621
3161
  exports.rateLimit = rateLimit;
3162
+ exports.redactObjectPii = redactObjectPii;
3163
+ exports.redactPii = redactPii;
2622
3164
  exports.safeCors = safeCors;
2623
3165
  exports.safeLog = safeLog;
2624
3166
  exports.sanitizeCommand = sanitizeCommand;
2625
3167
  exports.sanitizeFilename = sanitizeFilename;
2626
3168
  exports.sanitizeHeaderValue = sanitizeHeaderValue;
2627
3169
  exports.sanitizeHeaders = sanitizeHeaders;
3170
+ exports.sanitizeJsonpCallback = sanitizeJsonpCallback;
2628
3171
  exports.sanitizeObject = sanitizeObject;
2629
3172
  exports.sanitizePath = sanitizePath;
2630
3173
  exports.sanitizeSql = sanitizeSql;
3174
+ exports.sanitizeSsti = sanitizeSsti;
2631
3175
  exports.sanitizeString = sanitizeString;
2632
3176
  exports.sanitizeXss = sanitizeXss;
3177
+ exports.sanitizeXxe = sanitizeXxe;
3178
+ exports.scanObjectPii = scanObjectPii;
3179
+ exports.scanPii = scanPii;
2633
3180
  exports.secureCookieDefaults = secureCookieDefaults;
2634
3181
  exports.securityHeaders = securityHeaders;
2635
3182
  exports.validate = validate;
3183
+ exports.validateCsrfToken = validateCsrfToken;
2636
3184
  exports.validateEmail = validateEmail;
2637
3185
  exports.validateFile = validateFile;
2638
3186
  exports.validateRedirect = validateRedirect;