@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.
- package/README.md +1 -1
- package/dist/core/constants.d.ts +2 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +11 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +11 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/index.js +125 -46
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +126 -47
- package/dist/index.mjs.map +1 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/index.js +62 -30
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +63 -31
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/sanitizers/encode.d.ts.map +1 -1
- package/dist/sanitizers/index.d.ts +1 -0
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +90 -32
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +88 -33
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/ldap.d.ts +42 -0
- package/dist/sanitizers/ldap.d.ts.map +1 -0
- package/dist/sanitizers/path.d.ts.map +1 -1
- package/dist/sanitizers/sanitize.d.ts.map +1 -1
- package/dist/sanitizers/ssti.d.ts.map +1 -1
- package/dist/stores/index.js +21 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs +21 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/stores/memory.d.ts +4 -10
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/validation/index.js +38 -21
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +38 -21
- package/dist/validation/index.mjs.map +1 -1
- 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
|
|
165
|
-
/%0[
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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 (
|
|
705
|
-
|
|
706
|
-
if (
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
1304
|
-
const
|
|
1305
|
-
if (
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
result +=
|
|
1309
|
-
} else if (
|
|
1310
|
-
result += `\\x${
|
|
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${
|
|
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
|
-
|
|
1766
|
-
|
|
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
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
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
|
-
|
|
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) {
|