@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { promises } from 'dns';
|
|
2
|
-
import { randomBytes, createHash } from 'crypto';
|
|
2
|
+
import { randomBytes, timingSafeEqual, createHash } from 'crypto';
|
|
3
3
|
|
|
4
4
|
// src/core/constants.ts
|
|
5
5
|
var INPUT = {
|
|
@@ -66,7 +66,15 @@ var XSS_PATTERNS = [
|
|
|
66
66
|
/** URL-encoded script tags */
|
|
67
67
|
/%3Cscript/gi,
|
|
68
68
|
/** SVG with onload */
|
|
69
|
-
/<svg[^>]*onload/gi
|
|
69
|
+
/<svg[^>]*onload/gi,
|
|
70
|
+
/** form tags — phishing/credential harvesting via action= redirection */
|
|
71
|
+
/<form[\s>]/gi,
|
|
72
|
+
/** meta tags — http-equiv refresh redirects or CSP bypass */
|
|
73
|
+
/<meta[\s>]/gi,
|
|
74
|
+
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
75
|
+
/<base[\s>]/gi,
|
|
76
|
+
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
77
|
+
/<link[\s>]/gi
|
|
70
78
|
];
|
|
71
79
|
var XSS_REMOVE_PATTERNS = [
|
|
72
80
|
/** Full script blocks (content + tags) */
|
|
@@ -93,7 +101,15 @@ var XSS_REMOVE_PATTERNS = [
|
|
|
93
101
|
/javascript\s*:/gi,
|
|
94
102
|
/vbscript\s*:/gi,
|
|
95
103
|
/** data: URIs with HTML/script content */
|
|
96
|
-
/data\s*:\s*text\/html[^>\s]*/gi
|
|
104
|
+
/data\s*:\s*text\/html[^>\s]*/gi,
|
|
105
|
+
/** form tag injection — phishing via action= redirection */
|
|
106
|
+
/<form[\s>][^>]*/gi,
|
|
107
|
+
/** meta tag injection — http-equiv refresh or CSP bypass */
|
|
108
|
+
/<meta[\s>][^>]*/gi,
|
|
109
|
+
/** base href hijacking */
|
|
110
|
+
/<base[\s>][^>]*/gi,
|
|
111
|
+
/** link tag injection — stylesheet or preload attacks */
|
|
112
|
+
/<link[\s>][^>]*/gi
|
|
97
113
|
];
|
|
98
114
|
var SQL_PATTERNS = [
|
|
99
115
|
/** SQL keywords */
|
|
@@ -157,8 +173,8 @@ var COMMAND_PATTERNS = [
|
|
|
157
173
|
/[;&|`]/g,
|
|
158
174
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
159
175
|
/\$\(/g,
|
|
160
|
-
/** URL-encoded
|
|
161
|
-
/%0[
|
|
176
|
+
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
177
|
+
/%0[0-9a-f]/gi
|
|
162
178
|
];
|
|
163
179
|
var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
|
|
164
180
|
"__proto__",
|
|
@@ -449,7 +465,24 @@ function createRateLimiter(options = {}) {
|
|
|
449
465
|
}
|
|
450
466
|
next();
|
|
451
467
|
} catch (error) {
|
|
452
|
-
console.error("[arcis] Rate limiter error:", error);
|
|
468
|
+
console.error("[arcis] Rate limiter store error, using in-memory fallback:", error);
|
|
469
|
+
try {
|
|
470
|
+
const key = keyGenerator(req);
|
|
471
|
+
const now = Date.now();
|
|
472
|
+
if (!inMemoryStore[key] || inMemoryStore[key].resetTime < now) {
|
|
473
|
+
inMemoryStore[key] = { count: 1, resetTime: now + windowMs };
|
|
474
|
+
} else {
|
|
475
|
+
inMemoryStore[key].count++;
|
|
476
|
+
}
|
|
477
|
+
const count = inMemoryStore[key].count;
|
|
478
|
+
if (count > max) {
|
|
479
|
+
const resetSeconds = Math.ceil((inMemoryStore[key].resetTime - now) / 1e3);
|
|
480
|
+
res.setHeader("Retry-After", resetSeconds.toString());
|
|
481
|
+
res.status(statusCode).json({ error: message, retryAfter: resetSeconds });
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
453
486
|
next();
|
|
454
487
|
}
|
|
455
488
|
};
|
|
@@ -693,26 +726,31 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
693
726
|
const threats = [];
|
|
694
727
|
let value = input;
|
|
695
728
|
let wasSanitized = false;
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
729
|
+
value = value.normalize("NFKC");
|
|
730
|
+
let prev;
|
|
731
|
+
do {
|
|
732
|
+
prev = value;
|
|
733
|
+
for (const pattern of PATH_PATTERNS) {
|
|
699
734
|
pattern.lastIndex = 0;
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
735
|
+
if (pattern.test(value)) {
|
|
736
|
+
pattern.lastIndex = 0;
|
|
737
|
+
if (collectThreats) {
|
|
738
|
+
const matches = value.match(pattern);
|
|
739
|
+
if (matches) {
|
|
740
|
+
for (const match of matches) {
|
|
741
|
+
threats.push({
|
|
742
|
+
type: "path_traversal",
|
|
743
|
+
pattern: pattern.source,
|
|
744
|
+
original: match
|
|
745
|
+
});
|
|
746
|
+
}
|
|
709
747
|
}
|
|
710
748
|
}
|
|
749
|
+
value = value.replace(pattern, "");
|
|
750
|
+
wasSanitized = true;
|
|
711
751
|
}
|
|
712
|
-
value = value.replace(pattern, "");
|
|
713
|
-
wasSanitized = true;
|
|
714
752
|
}
|
|
715
|
-
}
|
|
753
|
+
} while (value !== prev);
|
|
716
754
|
if (collectThreats) {
|
|
717
755
|
return { value, wasSanitized, threats };
|
|
718
756
|
}
|
|
@@ -720,9 +758,10 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
720
758
|
}
|
|
721
759
|
function detectPathTraversal(input) {
|
|
722
760
|
if (typeof input !== "string") return false;
|
|
761
|
+
const normalized = input.normalize("NFKC");
|
|
723
762
|
for (const pattern of PATH_PATTERNS) {
|
|
724
763
|
pattern.lastIndex = 0;
|
|
725
|
-
if (pattern.test(
|
|
764
|
+
if (pattern.test(normalized)) {
|
|
726
765
|
return true;
|
|
727
766
|
}
|
|
728
767
|
}
|
|
@@ -780,7 +819,7 @@ function sanitizeString(value, options = {}) {
|
|
|
780
819
|
if (value.length > maxSize) {
|
|
781
820
|
throw new InputTooLargeError(maxSize, value.length);
|
|
782
821
|
}
|
|
783
|
-
const reject = options.mode
|
|
822
|
+
const reject = options.mode === "reject";
|
|
784
823
|
let result = value;
|
|
785
824
|
if (options.sql !== false) {
|
|
786
825
|
if (reject) {
|
|
@@ -929,10 +968,22 @@ var SSTI_DETECT_PATTERNS = [
|
|
|
929
968
|
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
930
969
|
];
|
|
931
970
|
var SSTI_REMOVE_PATTERNS = [
|
|
971
|
+
/** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
|
|
932
972
|
/\{\{.*?\}\}/g,
|
|
933
|
-
|
|
973
|
+
/**
|
|
974
|
+
* Freemarker / Spring EL: ${...} — only strip when the expression contains
|
|
975
|
+
* operators (?!*+-/), method calls (), or known-dangerous prefixes.
|
|
976
|
+
* Bare ${name} and ${user.name} are left intact (JS template literal syntax).
|
|
977
|
+
*/
|
|
978
|
+
/\$\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
979
|
+
/** ERB / EJS: <%= ... %> */
|
|
934
980
|
/<%[=\-]?.*?%>/gs,
|
|
935
|
-
|
|
981
|
+
/**
|
|
982
|
+
* Pug / Jade: #{...} — same narrowing as ${ above.
|
|
983
|
+
* #{name} output expressions are left intact.
|
|
984
|
+
*/
|
|
985
|
+
/#\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
986
|
+
/** Python dunder sandbox escape — always strip */
|
|
936
987
|
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
937
988
|
];
|
|
938
989
|
function sanitizeSsti(input, collectThreats = false) {
|
|
@@ -1296,16 +1347,18 @@ function encodeForAttribute(value) {
|
|
|
1296
1347
|
function encodeForJs(value) {
|
|
1297
1348
|
if (!value) return "";
|
|
1298
1349
|
let result = "";
|
|
1299
|
-
for (
|
|
1300
|
-
const
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
result +=
|
|
1305
|
-
} else if (
|
|
1306
|
-
result += `\\x${
|
|
1350
|
+
for (const char of value) {
|
|
1351
|
+
const cp = char.codePointAt(0);
|
|
1352
|
+
if (cp >= 48 && cp <= 57 || // 0-9
|
|
1353
|
+
cp >= 65 && cp <= 90 || // A-Z
|
|
1354
|
+
cp >= 97 && cp <= 122) {
|
|
1355
|
+
result += char;
|
|
1356
|
+
} else if (cp < 256) {
|
|
1357
|
+
result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
1358
|
+
} else if (cp <= 65535) {
|
|
1359
|
+
result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
1307
1360
|
} else {
|
|
1308
|
-
result += `\\u${
|
|
1361
|
+
result += `\\u{${cp.toString(16).toUpperCase()}}`;
|
|
1309
1362
|
}
|
|
1310
1363
|
}
|
|
1311
1364
|
return result;
|
|
@@ -1758,8 +1811,12 @@ function checkPrivateIp(hostname) {
|
|
|
1758
1811
|
if (hostname === "metadata.google.internal" || hostname === "metadata.internal" || hostname === "metadata.azure.internal") {
|
|
1759
1812
|
return "cloud metadata endpoint";
|
|
1760
1813
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1814
|
+
let ipv6 = hostname.replace(/^\[|\]$/g, "");
|
|
1815
|
+
const zoneIdx = ipv6.indexOf("%");
|
|
1816
|
+
if (zoneIdx !== -1) {
|
|
1817
|
+
ipv6 = ipv6.slice(0, zoneIdx);
|
|
1818
|
+
}
|
|
1819
|
+
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)) {
|
|
1763
1820
|
return "private IPv6 address";
|
|
1764
1821
|
}
|
|
1765
1822
|
const mappedDotted = ipv6.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
@@ -2818,11 +2875,9 @@ function generateCsrfToken(length = 32) {
|
|
|
2818
2875
|
function validateCsrfToken(cookieToken, requestToken) {
|
|
2819
2876
|
if (!cookieToken || !requestToken) return false;
|
|
2820
2877
|
if (cookieToken.length !== requestToken.length) return false;
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
}
|
|
2825
|
-
return result === 0;
|
|
2878
|
+
const a = Buffer.from(cookieToken);
|
|
2879
|
+
const b = Buffer.from(requestToken);
|
|
2880
|
+
return timingSafeEqual(a, b);
|
|
2826
2881
|
}
|
|
2827
2882
|
function getRequestToken(req, headerName, fieldName) {
|
|
2828
2883
|
const headerToken = req.headers[headerName.toLowerCase()];
|
|
@@ -2831,10 +2886,6 @@ function getRequestToken(req, headerName, fieldName) {
|
|
|
2831
2886
|
const bodyToken = req.body[fieldName];
|
|
2832
2887
|
if (typeof bodyToken === "string" && bodyToken) return bodyToken;
|
|
2833
2888
|
}
|
|
2834
|
-
if (req.query && fieldName in req.query) {
|
|
2835
|
-
const queryToken = req.query[fieldName];
|
|
2836
|
-
if (typeof queryToken === "string" && queryToken) return queryToken;
|
|
2837
|
-
}
|
|
2838
2889
|
return void 0;
|
|
2839
2890
|
}
|
|
2840
2891
|
function csrfProtection(options = {}) {
|
|
@@ -2917,7 +2968,15 @@ function setCsrfCookie(res, name, token, opts) {
|
|
|
2917
2968
|
if (opts.secure) parts.push("Secure");
|
|
2918
2969
|
parts.push(`SameSite=${opts.sameSite}`);
|
|
2919
2970
|
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
2920
|
-
|
|
2971
|
+
const newCookie = parts.join("; ");
|
|
2972
|
+
const existing = res.getHeader("Set-Cookie");
|
|
2973
|
+
if (existing === void 0) {
|
|
2974
|
+
res.setHeader("Set-Cookie", newCookie);
|
|
2975
|
+
} else if (Array.isArray(existing)) {
|
|
2976
|
+
res.setHeader("Set-Cookie", [...existing, newCookie]);
|
|
2977
|
+
} else {
|
|
2978
|
+
res.setHeader("Set-Cookie", [existing, newCookie]);
|
|
2979
|
+
}
|
|
2921
2980
|
}
|
|
2922
2981
|
function escapeRegex(str) {
|
|
2923
2982
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -3101,8 +3160,9 @@ function fingerprint(req, options = {}) {
|
|
|
3101
3160
|
}
|
|
3102
3161
|
|
|
3103
3162
|
// src/stores/memory.ts
|
|
3163
|
+
var DEFAULT_MAX_SIZE2 = 1e4;
|
|
3104
3164
|
var MemoryStore = class {
|
|
3105
|
-
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS) {
|
|
3165
|
+
constructor(windowMs = RATE_LIMIT.DEFAULT_WINDOW_MS, maxSize = DEFAULT_MAX_SIZE2) {
|
|
3106
3166
|
this.store = /* @__PURE__ */ new Map();
|
|
3107
3167
|
this.cleanupInterval = null;
|
|
3108
3168
|
if (!Number.isFinite(windowMs) || windowMs < RATE_LIMIT.MIN_WINDOW_MS) {
|
|
@@ -3110,7 +3170,11 @@ var MemoryStore = class {
|
|
|
3110
3170
|
`MemoryStore: windowMs must be a finite number >= ${RATE_LIMIT.MIN_WINDOW_MS} (got ${windowMs})`
|
|
3111
3171
|
);
|
|
3112
3172
|
}
|
|
3173
|
+
if (!Number.isFinite(maxSize) || maxSize < 1) {
|
|
3174
|
+
throw new RangeError(`MemoryStore: maxSize must be >= 1 (got ${maxSize})`);
|
|
3175
|
+
}
|
|
3113
3176
|
this.windowMs = windowMs;
|
|
3177
|
+
this.maxSize = maxSize;
|
|
3114
3178
|
this.startCleanup();
|
|
3115
3179
|
}
|
|
3116
3180
|
/**
|
|
@@ -3142,18 +3206,33 @@ var MemoryStore = class {
|
|
|
3142
3206
|
return entry;
|
|
3143
3207
|
}
|
|
3144
3208
|
async set(key, entry) {
|
|
3209
|
+
if (!this.store.has(key) && this.store.size >= this.maxSize) {
|
|
3210
|
+
this.evictExpired();
|
|
3211
|
+
if (this.store.size >= this.maxSize) return;
|
|
3212
|
+
}
|
|
3145
3213
|
this.store.set(key, entry);
|
|
3146
3214
|
}
|
|
3147
3215
|
async increment(key) {
|
|
3148
3216
|
const now = Date.now();
|
|
3149
3217
|
const entry = this.store.get(key);
|
|
3150
3218
|
if (!entry || entry.resetTime < now) {
|
|
3219
|
+
if (this.store.size >= this.maxSize) {
|
|
3220
|
+
this.evictExpired();
|
|
3221
|
+
if (this.store.size >= this.maxSize) return 1;
|
|
3222
|
+
}
|
|
3151
3223
|
this.store.set(key, { count: 1, resetTime: now + this.windowMs });
|
|
3152
3224
|
return 1;
|
|
3153
3225
|
}
|
|
3154
3226
|
entry.count++;
|
|
3155
3227
|
return entry.count;
|
|
3156
3228
|
}
|
|
3229
|
+
/** Eagerly remove expired entries to reclaim capacity. */
|
|
3230
|
+
evictExpired() {
|
|
3231
|
+
const now = Date.now();
|
|
3232
|
+
for (const [key, entry] of this.store.entries()) {
|
|
3233
|
+
if (entry.resetTime < now) this.store.delete(key);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3157
3236
|
async decrement(key) {
|
|
3158
3237
|
const entry = this.store.get(key);
|
|
3159
3238
|
if (entry && entry.count > 0) {
|