@arcis/node 1.4.3 → 1.4.4
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 +11 -3
- package/dist/cli/arcis.d.ts +23 -0
- package/dist/cli/arcis.d.ts.map +1 -0
- package/dist/cli/arcis.js +312 -0
- package/dist/cli/arcis.js.map +1 -0
- package/dist/cli/arcis.mjs +309 -0
- package/dist/cli/arcis.mjs.map +1 -0
- package/dist/core/constants.d.ts +1 -1
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +11 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +253 -141
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +253 -141
- 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/bot-detection.d.ts.map +1 -1
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/index.js +224 -3
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +224 -3
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/main.d.ts.map +1 -1
- package/dist/sanitizers/index.d.ts +2 -1
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +213 -145
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +213 -146
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/sanitize.d.ts +13 -0
- package/dist/sanitizers/sanitize.d.ts.map +1 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/telemetry/client.d.ts +3 -0
- package/dist/telemetry/client.d.ts.map +1 -1
- package/dist/telemetry/types.d.ts +12 -0
- package/dist/telemetry/types.d.ts.map +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +4 -1
|
@@ -40,6 +40,39 @@ var HEADERS = {
|
|
|
40
40
|
/** Default Cache-Control value for security */
|
|
41
41
|
CACHE_CONTROL: "no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
42
42
|
};
|
|
43
|
+
var XSS_PATTERNS = [
|
|
44
|
+
/** Script tags (ReDoS-safe version) */
|
|
45
|
+
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
46
|
+
/** javascript: protocol (allow optional spaces before colon) */
|
|
47
|
+
/javascript\s*:/gi,
|
|
48
|
+
/** vbscript: protocol */
|
|
49
|
+
/vbscript\s*:/gi,
|
|
50
|
+
/** Event handlers (onclick, onerror, etc.) — any separator before attribute */
|
|
51
|
+
/(?:[\s/])on\w+\s*=/gi,
|
|
52
|
+
/** iframe tags */
|
|
53
|
+
/<iframe/gi,
|
|
54
|
+
/** object tags */
|
|
55
|
+
/<object/gi,
|
|
56
|
+
/** embed tags */
|
|
57
|
+
/<embed/gi,
|
|
58
|
+
/** data: URIs (only dangerous ones, avoid false positives) */
|
|
59
|
+
/(?:^|[\s"'=])data:/gi,
|
|
60
|
+
/** URL-encoded script tags */
|
|
61
|
+
/%3Cscript/gi,
|
|
62
|
+
/** SVG with onload */
|
|
63
|
+
/<svg[^>]*onload/gi,
|
|
64
|
+
/** form tags — phishing/credential harvesting via action= redirection */
|
|
65
|
+
/<form[\s>]/gi,
|
|
66
|
+
/** meta tags — http-equiv refresh redirects or CSP bypass */
|
|
67
|
+
/<meta[\s>]/gi,
|
|
68
|
+
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
69
|
+
/<base[\s>]/gi,
|
|
70
|
+
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
71
|
+
/<link[\s>]/gi,
|
|
72
|
+
/** style tag — CSS expression() / behavior: / IE-era attacks. Mirrors
|
|
73
|
+
* Python's xss-style-tag from packages/core/patterns.json. */
|
|
74
|
+
/<style[\s>]/gi
|
|
75
|
+
];
|
|
43
76
|
var XSS_REMOVE_PATTERNS = [
|
|
44
77
|
/** Full script blocks (content + tags) */
|
|
45
78
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
@@ -677,6 +710,20 @@ function sanitizeXss(input, collectThreats = false, htmlEncode = false) {
|
|
|
677
710
|
}
|
|
678
711
|
return value;
|
|
679
712
|
}
|
|
713
|
+
function detectXss(input) {
|
|
714
|
+
if (typeof input !== "string") return false;
|
|
715
|
+
if (/\s+on\w+\s*=/i.test(input)) return true;
|
|
716
|
+
if (/javascript\s*:/i.test(input)) return true;
|
|
717
|
+
if (/vbscript\s*:/i.test(input)) return true;
|
|
718
|
+
if (/data\s*:\s*text\/html/i.test(input)) return true;
|
|
719
|
+
for (const pattern of XSS_PATTERNS) {
|
|
720
|
+
pattern.lastIndex = 0;
|
|
721
|
+
if (pattern.test(input)) {
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
680
727
|
|
|
681
728
|
// src/sanitizers/sql.ts
|
|
682
729
|
function sanitizeSql(input, collectThreats = false) {
|
|
@@ -760,6 +807,17 @@ function sanitizePath(input, collectThreats = false) {
|
|
|
760
807
|
}
|
|
761
808
|
return value;
|
|
762
809
|
}
|
|
810
|
+
function detectPathTraversal(input) {
|
|
811
|
+
if (typeof input !== "string") return false;
|
|
812
|
+
const normalized = input.normalize("NFKC");
|
|
813
|
+
for (const pattern of PATH_PATTERNS) {
|
|
814
|
+
pattern.lastIndex = 0;
|
|
815
|
+
if (pattern.test(normalized)) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
763
821
|
|
|
764
822
|
// src/sanitizers/command.ts
|
|
765
823
|
function sanitizeCommand(input, collectThreats = false) {
|
|
@@ -805,6 +863,60 @@ function detectCommandInjection(input) {
|
|
|
805
863
|
return false;
|
|
806
864
|
}
|
|
807
865
|
|
|
866
|
+
// src/sanitizers/ssti.ts
|
|
867
|
+
var SSTI_DETECT_PATTERNS = [
|
|
868
|
+
/** Jinja2 / Twig / Nunjucks: {{ ... }} */
|
|
869
|
+
/\{\{.*?\}\}/g,
|
|
870
|
+
/** Freemarker / Thymeleaf / Spring EL: ${ ... } */
|
|
871
|
+
/\$\{.*?\}/g,
|
|
872
|
+
/** ERB / EJS: <%= ... %> or <% ... %> */
|
|
873
|
+
/<%[=\-]?.*?%>/gs,
|
|
874
|
+
/** Pug / Jade / Slim: #{ ... } */
|
|
875
|
+
/#\{.*?\}/g,
|
|
876
|
+
/** Python dunder sandbox escape */
|
|
877
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi,
|
|
878
|
+
/** Jinja2 config leak: {{config.X}} or {{config['X']}} */
|
|
879
|
+
/\{\{\s*config[.\[]/gi,
|
|
880
|
+
/** Jinja2 built-in objects */
|
|
881
|
+
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
882
|
+
];
|
|
883
|
+
function detectSsti(input) {
|
|
884
|
+
if (typeof input !== "string") return false;
|
|
885
|
+
for (const pattern of SSTI_DETECT_PATTERNS) {
|
|
886
|
+
pattern.lastIndex = 0;
|
|
887
|
+
if (pattern.test(input)) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/sanitizers/xxe.ts
|
|
895
|
+
var XXE_DETECT_PATTERNS = [
|
|
896
|
+
/** DOCTYPE declaration */
|
|
897
|
+
/<!DOCTYPE\b/gi,
|
|
898
|
+
/** ENTITY declaration */
|
|
899
|
+
/<!ENTITY\b/gi,
|
|
900
|
+
/** SYSTEM keyword with URI */
|
|
901
|
+
/\bSYSTEM\s+["']/gi,
|
|
902
|
+
/** PUBLIC keyword with URI */
|
|
903
|
+
/\bPUBLIC\s+["']/gi,
|
|
904
|
+
/** Parameter entity reference (%entity;) */
|
|
905
|
+
/%\s*\w+\s*;/g,
|
|
906
|
+
/** CDATA section (often used to smuggle payloads) */
|
|
907
|
+
/<!\[CDATA\[/gi
|
|
908
|
+
];
|
|
909
|
+
function detectXxe(input) {
|
|
910
|
+
if (typeof input !== "string") return false;
|
|
911
|
+
for (const pattern of XXE_DETECT_PATTERNS) {
|
|
912
|
+
pattern.lastIndex = 0;
|
|
913
|
+
if (pattern.test(input)) {
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
|
|
808
920
|
// src/sanitizers/sanitize.ts
|
|
809
921
|
function sanitizeString(value, options = {}) {
|
|
810
922
|
if (typeof value !== "string") return value;
|
|
@@ -874,9 +986,73 @@ function sanitizeObjectDepth(obj, options, depth) {
|
|
|
874
986
|
}
|
|
875
987
|
return result;
|
|
876
988
|
}
|
|
989
|
+
function scanThreats(data, depth = 0) {
|
|
990
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return null;
|
|
991
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
992
|
+
for (const key of Object.keys(data)) {
|
|
993
|
+
const lower = key.toLowerCase();
|
|
994
|
+
if (DANGEROUS_PROTO_KEYS.has(lower)) {
|
|
995
|
+
return { vector: "prototype", rule: "prototype/match", matchedPattern: key };
|
|
996
|
+
}
|
|
997
|
+
if (NOSQL_DANGEROUS_KEYS.has(key)) {
|
|
998
|
+
return { vector: "nosql", rule: "nosql/match", matchedPattern: key };
|
|
999
|
+
}
|
|
1000
|
+
const inner = scanThreats(data[key], depth + 1);
|
|
1001
|
+
if (inner) return inner;
|
|
1002
|
+
}
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
if (Array.isArray(data)) {
|
|
1006
|
+
for (const item of data) {
|
|
1007
|
+
const inner = scanThreats(item, depth + 1);
|
|
1008
|
+
if (inner) return inner;
|
|
1009
|
+
}
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof data !== "string") return null;
|
|
1013
|
+
const sample = data.slice(0, 80);
|
|
1014
|
+
if (detectXss(data)) {
|
|
1015
|
+
return { vector: "xss", rule: "xss/match", matchedPattern: sample };
|
|
1016
|
+
}
|
|
1017
|
+
if (detectSsti(data)) {
|
|
1018
|
+
return { vector: "ssti", rule: "ssti/match", matchedPattern: sample };
|
|
1019
|
+
}
|
|
1020
|
+
if (detectXxe(data)) {
|
|
1021
|
+
return { vector: "xxe", rule: "xxe/match", matchedPattern: sample };
|
|
1022
|
+
}
|
|
1023
|
+
if (detectSql(data)) {
|
|
1024
|
+
return { vector: "sql", rule: "sql/match", matchedPattern: sample };
|
|
1025
|
+
}
|
|
1026
|
+
if (detectPathTraversal(data)) {
|
|
1027
|
+
return { vector: "path", rule: "path/match", matchedPattern: sample };
|
|
1028
|
+
}
|
|
1029
|
+
if (detectCommandInjection(data)) {
|
|
1030
|
+
return { vector: "command", rule: "command/match", matchedPattern: sample };
|
|
1031
|
+
}
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
877
1034
|
function createSanitizer(options = {}) {
|
|
878
|
-
return (req,
|
|
1035
|
+
return (req, res, next) => {
|
|
879
1036
|
try {
|
|
1037
|
+
if (options.block) {
|
|
1038
|
+
const hit = scanThreats(req.body) || scanThreats(req.query) || scanThreats(req.params) || scanThreats(req.path);
|
|
1039
|
+
if (hit) {
|
|
1040
|
+
req.__arcis = {
|
|
1041
|
+
vector: hit.vector,
|
|
1042
|
+
rule: hit.rule,
|
|
1043
|
+
severity: "high",
|
|
1044
|
+
matchedPattern: hit.matchedPattern,
|
|
1045
|
+
reason: `${hit.vector} pattern detected in request`,
|
|
1046
|
+
decision: "deny"
|
|
1047
|
+
};
|
|
1048
|
+
res.status(403).json({
|
|
1049
|
+
error: "Request blocked for security reasons",
|
|
1050
|
+
code: "SECURITY_THREAT",
|
|
1051
|
+
vector: hit.vector
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
880
1056
|
if (req.body && typeof req.body === "object") {
|
|
881
1057
|
req.body = sanitizeObject(req.body, options);
|
|
882
1058
|
}
|
|
@@ -1376,11 +1552,16 @@ var MAX_BATCH_SIZE = 500;
|
|
|
1376
1552
|
var DEFAULT_FLUSH_INTERVAL_MS = 5e3;
|
|
1377
1553
|
var MIN_FLUSH_INTERVAL_MS = 500;
|
|
1378
1554
|
var FLUSH_TIMEOUT_MS = 1e4;
|
|
1555
|
+
var DEFAULT_MAX_QUEUE_SIZE = 1e4;
|
|
1379
1556
|
var TelemetryClient = class {
|
|
1380
1557
|
constructor(options) {
|
|
1381
1558
|
this.queue = [];
|
|
1382
1559
|
this.flushing = false;
|
|
1383
1560
|
this.closed = false;
|
|
1561
|
+
// Counts events dropped since the last successful flush. Resets to 0
|
|
1562
|
+
// each flush so onQueueOverflow callbacks see "drops in this window"
|
|
1563
|
+
// rather than a monotonic lifetime counter.
|
|
1564
|
+
this.droppedSinceLastFlush = 0;
|
|
1384
1565
|
if (!options.endpoint || typeof options.endpoint !== "string") {
|
|
1385
1566
|
throw new TypeError("TelemetryClient: `endpoint` is required");
|
|
1386
1567
|
}
|
|
@@ -1396,8 +1577,14 @@ var TelemetryClient = class {
|
|
|
1396
1577
|
options.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS,
|
|
1397
1578
|
MIN_FLUSH_INTERVAL_MS
|
|
1398
1579
|
);
|
|
1580
|
+
this.maxQueueSize = Math.max(
|
|
1581
|
+
options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE,
|
|
1582
|
+
this.batchSize
|
|
1583
|
+
);
|
|
1399
1584
|
this.onError = options.onError ?? (() => {
|
|
1400
1585
|
});
|
|
1586
|
+
this.onQueueOverflow = options.onQueueOverflow ?? (() => {
|
|
1587
|
+
});
|
|
1401
1588
|
this.startTimer();
|
|
1402
1589
|
}
|
|
1403
1590
|
/**
|
|
@@ -1407,6 +1594,15 @@ var TelemetryClient = class {
|
|
|
1407
1594
|
record(event) {
|
|
1408
1595
|
if (this.closed) return;
|
|
1409
1596
|
this.queue.push(event);
|
|
1597
|
+
if (this.queue.length > this.maxQueueSize) {
|
|
1598
|
+
const drop = this.queue.length - this.maxQueueSize;
|
|
1599
|
+
this.queue.splice(0, drop);
|
|
1600
|
+
this.droppedSinceLastFlush += drop;
|
|
1601
|
+
try {
|
|
1602
|
+
this.onQueueOverflow(this.droppedSinceLastFlush);
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1410
1606
|
if (this.queue.length >= this.batchSize) {
|
|
1411
1607
|
void this.flush();
|
|
1412
1608
|
}
|
|
@@ -1423,6 +1619,7 @@ var TelemetryClient = class {
|
|
|
1423
1619
|
try {
|
|
1424
1620
|
const batch = this.queue.splice(0, this.batchSize);
|
|
1425
1621
|
await this.send(batch);
|
|
1622
|
+
this.droppedSinceLastFlush = 0;
|
|
1426
1623
|
} catch (err) {
|
|
1427
1624
|
this.safeNotify(err);
|
|
1428
1625
|
} finally {
|
|
@@ -1567,7 +1764,10 @@ function arcis(options = {}) {
|
|
|
1567
1764
|
cleanupFns.push(() => rateLimiter.close());
|
|
1568
1765
|
}
|
|
1569
1766
|
if (options.sanitize !== false) {
|
|
1570
|
-
const sanitizeOpts = typeof options.sanitize === "object" ? options.sanitize : {};
|
|
1767
|
+
const sanitizeOpts = typeof options.sanitize === "object" ? { ...options.sanitize } : {};
|
|
1768
|
+
if (options.block && sanitizeOpts.block === void 0) {
|
|
1769
|
+
sanitizeOpts.block = true;
|
|
1770
|
+
}
|
|
1571
1771
|
const sanitizer = createSanitizer(sanitizeOpts);
|
|
1572
1772
|
middlewares.push(telemetryClient ? tapSanitizerThreats(sanitizer) : sanitizer);
|
|
1573
1773
|
}
|
|
@@ -2094,6 +2294,13 @@ function botProtection(options = {}) {
|
|
|
2094
2294
|
return next();
|
|
2095
2295
|
}
|
|
2096
2296
|
if (denySet.has(result.category)) {
|
|
2297
|
+
req.__arcis = {
|
|
2298
|
+
vector: "bot",
|
|
2299
|
+
rule: `bot/${result.category.toLowerCase()}`,
|
|
2300
|
+
severity: "medium",
|
|
2301
|
+
reason: result.name ? `Bot detected: ${result.name}` : "Bot detected",
|
|
2302
|
+
decision: "deny"
|
|
2303
|
+
};
|
|
2097
2304
|
if (onDetected) {
|
|
2098
2305
|
return onDetected(req, res, result);
|
|
2099
2306
|
}
|
|
@@ -2101,6 +2308,13 @@ function botProtection(options = {}) {
|
|
|
2101
2308
|
return;
|
|
2102
2309
|
}
|
|
2103
2310
|
if (defaultAction === "deny") {
|
|
2311
|
+
req.__arcis = {
|
|
2312
|
+
vector: "bot",
|
|
2313
|
+
rule: "bot/uncategorized",
|
|
2314
|
+
severity: "medium",
|
|
2315
|
+
reason: "Uncategorized bot under defaultAction=deny",
|
|
2316
|
+
decision: "deny"
|
|
2317
|
+
};
|
|
2104
2318
|
if (onDetected) {
|
|
2105
2319
|
return onDetected(req, res, result);
|
|
2106
2320
|
}
|
|
@@ -2154,7 +2368,14 @@ function csrfProtection(options = {}) {
|
|
|
2154
2368
|
sameSite: options.cookie?.sameSite ?? "Lax",
|
|
2155
2369
|
domain: options.cookie?.domain
|
|
2156
2370
|
};
|
|
2157
|
-
const defaultOnError = (
|
|
2371
|
+
const defaultOnError = (req, res, _next) => {
|
|
2372
|
+
req.__arcis = {
|
|
2373
|
+
vector: "csrf",
|
|
2374
|
+
rule: "csrf/token-mismatch",
|
|
2375
|
+
severity: "high",
|
|
2376
|
+
reason: "CSRF token missing or invalid",
|
|
2377
|
+
decision: "deny"
|
|
2378
|
+
};
|
|
2158
2379
|
res.status(403).json({
|
|
2159
2380
|
error: "CSRF token validation failed",
|
|
2160
2381
|
message: "Invalid or missing CSRF token. Include the token from the cookie in the X-CSRF-Token header."
|