@arcis/node 1.5.2 → 1.6.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.
- package/README.md +48 -7
- package/dist/astro/index.js.map +1 -1
- package/dist/astro/index.mjs.map +1 -1
- package/dist/bun/index.js.map +1 -1
- package/dist/bun/index.mjs.map +1 -1
- package/dist/core/constants.d.ts +2 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +19 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +19 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/fastify/index.js.map +1 -1
- package/dist/fastify/index.mjs.map +1 -1
- package/dist/hono/index.js.map +1 -1
- package/dist/hono/index.mjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +407 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +407 -9
- package/dist/index.mjs.map +1 -1
- package/dist/koa/index.js.map +1 -1
- package/dist/koa/index.mjs.map +1 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/middleware/correlation.d.ts +87 -0
- package/dist/middleware/correlation.d.ts.map +1 -0
- package/dist/middleware/graphql.d.ts.map +1 -1
- package/dist/middleware/index.d.ts +3 -1
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +366 -8
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +366 -9
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/protect.d.ts +32 -0
- package/dist/middleware/protect.d.ts.map +1 -1
- package/dist/nestjs/index.js +55 -2
- package/dist/nestjs/index.js.map +1 -1
- package/dist/nestjs/index.mjs +55 -2
- package/dist/nestjs/index.mjs.map +1 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/index.mjs.map +1 -1
- package/dist/nuxt/index.js.map +1 -1
- package/dist/nuxt/index.mjs.map +1 -1
- package/dist/sanitizers/deserialization.d.ts +30 -0
- package/dist/sanitizers/deserialization.d.ts.map +1 -0
- package/dist/sanitizers/graphql.d.ts +20 -3
- package/dist/sanitizers/graphql.d.ts.map +1 -1
- package/dist/sanitizers/index.d.ts +2 -0
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +150 -7
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +149 -8
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/prompt-injection.d.ts.map +1 -1
- package/dist/sanitizers/sanitize.d.ts +0 -20
- 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/sveltekit/index.js.map +1 -1
- package/dist/sveltekit/index.mjs.map +1 -1
- package/dist/validation/index.js +55 -2
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +55 -2
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/middleware/index.js
CHANGED
|
@@ -140,7 +140,16 @@ var SQL_PATTERNS = [
|
|
|
140
140
|
/** Time-based blind: PostgreSQL pg_sleep() */
|
|
141
141
|
/\bpg_sleep\s*\(/gi,
|
|
142
142
|
/** Time-based blind: MSSQL WAITFOR DELAY */
|
|
143
|
-
/\bWAITFOR\s+DELAY\b/gi
|
|
143
|
+
/\bWAITFOR\s+DELAY\b/gi,
|
|
144
|
+
/**
|
|
145
|
+
* Oracle DBMS_* stdlib packages used for time-based blind SQLi
|
|
146
|
+
* (DBMS_LOCK.SLEEP, DBMS_PIPE.RECEIVE_MESSAGE) and other Oracle
|
|
147
|
+
* abuse paths. No legitimate user input contains these. Mirrors
|
|
148
|
+
* `sqli-oracle-dbms-packages` in packages/core/patterns.json —
|
|
149
|
+
* improvements.md §1.1.e Q3. Must stay in sync until Node
|
|
150
|
+
* migrates to patterns.json-at-runtime (planned v1.7).
|
|
151
|
+
*/
|
|
152
|
+
/\bDBMS_(?:LOCK|PIPE|UTILITY|XSLPROCESSOR|JAVA|OUTPUT|SCHEDULER)\b/gi
|
|
144
153
|
];
|
|
145
154
|
var PATH_PATTERNS = [
|
|
146
155
|
/** Unix path traversal */
|
|
@@ -178,6 +187,15 @@ var COMMAND_PATTERNS = [
|
|
|
178
187
|
/[;&|`]/g,
|
|
179
188
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
180
189
|
/\$\(/g,
|
|
190
|
+
/**
|
|
191
|
+
* POSIX shell IFS-substitution: ${IFS} or ${IFS%??}.
|
|
192
|
+
* Attackers use this to inject spaces past metacharacter filters
|
|
193
|
+
* in payloads like `;cat${IFS}/etc/passwd`. Mirrors
|
|
194
|
+
* `cmdi-ifs-bypass` in packages/core/patterns.json — improvements.md
|
|
195
|
+
* §1.1.e Q5. Must stay in sync until Node migrates to
|
|
196
|
+
* patterns.json-at-runtime (planned v1.7).
|
|
197
|
+
*/
|
|
198
|
+
/\$\{IFS(?:%[^}]*)?\}/g,
|
|
181
199
|
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
182
200
|
/%0[0-9a-f]/gi
|
|
183
201
|
];
|
|
@@ -979,6 +997,40 @@ function detectHeaderInjection(input) {
|
|
|
979
997
|
}
|
|
980
998
|
|
|
981
999
|
// src/sanitizers/sanitize.ts
|
|
1000
|
+
function multiDecode(value, maxPasses = 4) {
|
|
1001
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
1002
|
+
const prev = value;
|
|
1003
|
+
try {
|
|
1004
|
+
value = decodeURIComponent(value);
|
|
1005
|
+
} catch {
|
|
1006
|
+
}
|
|
1007
|
+
value = htmlEntityDecode(value);
|
|
1008
|
+
if (value === prev) break;
|
|
1009
|
+
}
|
|
1010
|
+
return value;
|
|
1011
|
+
}
|
|
1012
|
+
function htmlEntityDecode(s) {
|
|
1013
|
+
s = s.replace(/&#(\d+);/g, (_m, n) => {
|
|
1014
|
+
const code = parseInt(n, 10);
|
|
1015
|
+
return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
|
|
1016
|
+
});
|
|
1017
|
+
s = s.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => {
|
|
1018
|
+
const code = parseInt(h, 16);
|
|
1019
|
+
return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
|
|
1020
|
+
});
|
|
1021
|
+
const named = {
|
|
1022
|
+
"<": "<",
|
|
1023
|
+
">": ">",
|
|
1024
|
+
"&": "&",
|
|
1025
|
+
""": '"',
|
|
1026
|
+
"'": "'",
|
|
1027
|
+
" ": " "
|
|
1028
|
+
};
|
|
1029
|
+
for (const [entity, ch] of Object.entries(named)) {
|
|
1030
|
+
s = s.split(entity).join(ch);
|
|
1031
|
+
}
|
|
1032
|
+
return s;
|
|
1033
|
+
}
|
|
982
1034
|
function sanitizeString(value, options = {}) {
|
|
983
1035
|
if (typeof value !== "string") return value;
|
|
984
1036
|
const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
|
|
@@ -986,7 +1038,8 @@ function sanitizeString(value, options = {}) {
|
|
|
986
1038
|
throw new InputTooLargeError(maxSize, value.length);
|
|
987
1039
|
}
|
|
988
1040
|
const reject = options.mode === "reject";
|
|
989
|
-
let result = value;
|
|
1041
|
+
let result = value.normalize("NFKC");
|
|
1042
|
+
result = multiDecode(result);
|
|
990
1043
|
if (options.sql !== false) {
|
|
991
1044
|
if (reject) {
|
|
992
1045
|
if (detectSql(result)) {
|
|
@@ -1145,7 +1198,9 @@ function createSanitizer(options = {}) {
|
|
|
1145
1198
|
var DEFAULTS = {
|
|
1146
1199
|
maxDepth: 10,
|
|
1147
1200
|
maxLength: 1e4,
|
|
1148
|
-
blockIntrospection: true
|
|
1201
|
+
blockIntrospection: true,
|
|
1202
|
+
maxAliases: 50,
|
|
1203
|
+
blockFragmentCycles: true
|
|
1149
1204
|
};
|
|
1150
1205
|
var INTROSPECTION_PATTERN = /\b__(schema|type|typeKind|directive)\b/;
|
|
1151
1206
|
function computeDepth(query) {
|
|
@@ -1162,22 +1217,87 @@ function computeDepth(query) {
|
|
|
1162
1217
|
}
|
|
1163
1218
|
return max;
|
|
1164
1219
|
}
|
|
1220
|
+
var ALIAS_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
1221
|
+
var FRAGMENT_DEF_PATTERN = /\bfragment\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+on\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\{/g;
|
|
1222
|
+
var FRAGMENT_SPREAD_PATTERN = /\.\.\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
1223
|
+
function countAliases(query) {
|
|
1224
|
+
let n = 0;
|
|
1225
|
+
ALIAS_PATTERN.lastIndex = 0;
|
|
1226
|
+
while (ALIAS_PATTERN.exec(query) !== null) n++;
|
|
1227
|
+
return n;
|
|
1228
|
+
}
|
|
1229
|
+
function hasFragmentCycle(query) {
|
|
1230
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1231
|
+
FRAGMENT_DEF_PATTERN.lastIndex = 0;
|
|
1232
|
+
let match;
|
|
1233
|
+
while ((match = FRAGMENT_DEF_PATTERN.exec(query)) !== null) {
|
|
1234
|
+
const name = match[1];
|
|
1235
|
+
const bodyStart = match.index + match[0].length;
|
|
1236
|
+
let depth = 1;
|
|
1237
|
+
let i = bodyStart;
|
|
1238
|
+
while (i < query.length && depth > 0) {
|
|
1239
|
+
const ch = query[i];
|
|
1240
|
+
if (ch === "{") depth++;
|
|
1241
|
+
else if (ch === "}") depth--;
|
|
1242
|
+
i++;
|
|
1243
|
+
}
|
|
1244
|
+
const bodyEnd = depth === 0 ? i - 1 : i;
|
|
1245
|
+
const body = query.slice(bodyStart, bodyEnd);
|
|
1246
|
+
const spreads = /* @__PURE__ */ new Set();
|
|
1247
|
+
FRAGMENT_SPREAD_PATTERN.lastIndex = 0;
|
|
1248
|
+
let sm;
|
|
1249
|
+
while ((sm = FRAGMENT_SPREAD_PATTERN.exec(body)) !== null) {
|
|
1250
|
+
spreads.add(sm[1]);
|
|
1251
|
+
}
|
|
1252
|
+
deps.set(name, spreads);
|
|
1253
|
+
}
|
|
1254
|
+
if (deps.size === 0) return false;
|
|
1255
|
+
const WHITE = 0;
|
|
1256
|
+
const GRAY = 1;
|
|
1257
|
+
const BLACK = 2;
|
|
1258
|
+
const color = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const name of deps.keys()) color.set(name, WHITE);
|
|
1260
|
+
function visit(name) {
|
|
1261
|
+
if (color.get(name) === GRAY) return true;
|
|
1262
|
+
if (color.get(name) === BLACK) return false;
|
|
1263
|
+
if (!deps.has(name)) return false;
|
|
1264
|
+
color.set(name, GRAY);
|
|
1265
|
+
for (const child of deps.get(name)) {
|
|
1266
|
+
if (visit(child)) return true;
|
|
1267
|
+
}
|
|
1268
|
+
color.set(name, BLACK);
|
|
1269
|
+
return false;
|
|
1270
|
+
}
|
|
1271
|
+
for (const name of deps.keys()) {
|
|
1272
|
+
if (visit(name)) return true;
|
|
1273
|
+
}
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1165
1276
|
function inspectGraphqlQuery(query, options = {}) {
|
|
1166
1277
|
const maxDepth = options.maxDepth ?? DEFAULTS.maxDepth;
|
|
1167
1278
|
const maxLength = options.maxLength ?? DEFAULTS.maxLength;
|
|
1168
1279
|
const blockIntrospection = options.blockIntrospection ?? DEFAULTS.blockIntrospection;
|
|
1280
|
+
const maxAliases = options.maxAliases ?? DEFAULTS.maxAliases;
|
|
1281
|
+
const blockFragmentCycles = options.blockFragmentCycles ?? DEFAULTS.blockFragmentCycles;
|
|
1169
1282
|
const length = query.length;
|
|
1170
1283
|
const depth = computeDepth(query);
|
|
1284
|
+
const aliases = countAliases(query);
|
|
1171
1285
|
if (depth > maxDepth) {
|
|
1172
|
-
return { blocked: true, reason: "depth", depth, length };
|
|
1286
|
+
return { blocked: true, reason: "depth", depth, length, aliases };
|
|
1173
1287
|
}
|
|
1174
1288
|
if (blockIntrospection && INTROSPECTION_PATTERN.test(query)) {
|
|
1175
|
-
return { blocked: true, reason: "introspection", depth, length };
|
|
1289
|
+
return { blocked: true, reason: "introspection", depth, length, aliases };
|
|
1290
|
+
}
|
|
1291
|
+
if (aliases > maxAliases) {
|
|
1292
|
+
return { blocked: true, reason: "aliases", depth, length, aliases };
|
|
1293
|
+
}
|
|
1294
|
+
if (blockFragmentCycles && hasFragmentCycle(query)) {
|
|
1295
|
+
return { blocked: true, reason: "fragment_cycle", depth, length, aliases };
|
|
1176
1296
|
}
|
|
1177
1297
|
if (length > maxLength) {
|
|
1178
|
-
return { blocked: true, reason: "length", depth, length };
|
|
1298
|
+
return { blocked: true, reason: "length", depth, length, aliases };
|
|
1179
1299
|
}
|
|
1180
|
-
return { blocked: false, depth, length };
|
|
1300
|
+
return { blocked: false, depth, length, aliases };
|
|
1181
1301
|
}
|
|
1182
1302
|
|
|
1183
1303
|
// src/validation/schema.ts
|
|
@@ -8691,6 +8811,46 @@ function massAssign(options) {
|
|
|
8691
8811
|
}
|
|
8692
8812
|
|
|
8693
8813
|
// src/middleware/protect.ts
|
|
8814
|
+
function getClientIp(req) {
|
|
8815
|
+
const xff = req?.headers?.["x-forwarded-for"] ?? req?.headers?.["X-Forwarded-For"];
|
|
8816
|
+
if (typeof xff === "string" && xff.length > 0) {
|
|
8817
|
+
const first = xff.split(",")[0]?.trim();
|
|
8818
|
+
if (first) return first;
|
|
8819
|
+
}
|
|
8820
|
+
if (typeof req?.ip === "string") return req.ip;
|
|
8821
|
+
const remote = req?.socket?.remoteAddress;
|
|
8822
|
+
return typeof remote === "string" ? remote : "";
|
|
8823
|
+
}
|
|
8824
|
+
function correlationMiddleware(opts) {
|
|
8825
|
+
const vector = opts.vector ?? "request";
|
|
8826
|
+
const usernameField = opts.usernameField ?? "username";
|
|
8827
|
+
const statusCode = opts.statusCode ?? 429;
|
|
8828
|
+
const message = opts.message ?? "Suspicious request pattern detected.";
|
|
8829
|
+
return function(req, res, next) {
|
|
8830
|
+
const ip = getClientIp(req);
|
|
8831
|
+
if (!ip) return next();
|
|
8832
|
+
const route = opts.route ?? (req.path || req.url || "/");
|
|
8833
|
+
const username = req?.body?.[usernameField];
|
|
8834
|
+
const distinctValue = typeof username === "string" && username.length > 0 ? username : void 0;
|
|
8835
|
+
const detections = opts.window.record(
|
|
8836
|
+
ip,
|
|
8837
|
+
vector,
|
|
8838
|
+
route,
|
|
8839
|
+
req.method || "GET",
|
|
8840
|
+
distinctValue
|
|
8841
|
+
);
|
|
8842
|
+
if (detections.scanner || detections.credentialStuffing || detections.raceWindow) {
|
|
8843
|
+
res.status(statusCode).json({
|
|
8844
|
+
error: message,
|
|
8845
|
+
scanner: detections.scanner,
|
|
8846
|
+
credential_stuffing: detections.credentialStuffing,
|
|
8847
|
+
race_window: detections.raceWindow
|
|
8848
|
+
});
|
|
8849
|
+
return;
|
|
8850
|
+
}
|
|
8851
|
+
next();
|
|
8852
|
+
};
|
|
8853
|
+
}
|
|
8694
8854
|
function resolve(override, defaults) {
|
|
8695
8855
|
if (override === false) return null;
|
|
8696
8856
|
if (override === void 0) return defaults;
|
|
@@ -8710,6 +8870,11 @@ function protectLogin(options = {}) {
|
|
|
8710
8870
|
if (csrf) middlewares.push(csrfProtection(csrf));
|
|
8711
8871
|
const sanitize = resolve(options.sanitize, {});
|
|
8712
8872
|
if (sanitize) middlewares.push(createSanitizer(sanitize));
|
|
8873
|
+
if (options.correlation) {
|
|
8874
|
+
middlewares.push(
|
|
8875
|
+
correlationMiddleware({ vector: "login", ...options.correlation })
|
|
8876
|
+
);
|
|
8877
|
+
}
|
|
8713
8878
|
return middlewares;
|
|
8714
8879
|
}
|
|
8715
8880
|
function protectSignup(options = {}) {
|
|
@@ -8726,6 +8891,11 @@ function protectSignup(options = {}) {
|
|
|
8726
8891
|
if (sanitize) middlewares.push(createSanitizer(sanitize));
|
|
8727
8892
|
const signup = resolve(options.signup, {});
|
|
8728
8893
|
if (signup) middlewares.push(signupProtection(signup));
|
|
8894
|
+
if (options.correlation) {
|
|
8895
|
+
middlewares.push(
|
|
8896
|
+
correlationMiddleware({ vector: "signup", ...options.correlation })
|
|
8897
|
+
);
|
|
8898
|
+
}
|
|
8729
8899
|
return middlewares;
|
|
8730
8900
|
}
|
|
8731
8901
|
function protectApi(options = {}) {
|
|
@@ -8736,6 +8906,11 @@ function protectApi(options = {}) {
|
|
|
8736
8906
|
if (cors) middlewares.push(safeCors(cors));
|
|
8737
8907
|
const sanitize = resolve(options.sanitize, {});
|
|
8738
8908
|
if (sanitize) middlewares.push(createSanitizer(sanitize));
|
|
8909
|
+
if (options.correlation) {
|
|
8910
|
+
middlewares.push(
|
|
8911
|
+
correlationMiddleware({ vector: "api", ...options.correlation })
|
|
8912
|
+
);
|
|
8913
|
+
}
|
|
8739
8914
|
return middlewares;
|
|
8740
8915
|
}
|
|
8741
8916
|
|
|
@@ -8743,7 +8918,9 @@ function protectApi(options = {}) {
|
|
|
8743
8918
|
var DEFAULT_MESSAGES = {
|
|
8744
8919
|
depth: "Query exceeds maximum nesting depth",
|
|
8745
8920
|
length: "Query exceeds maximum length",
|
|
8746
|
-
introspection: "Introspection queries are disabled"
|
|
8921
|
+
introspection: "Introspection queries are disabled",
|
|
8922
|
+
aliases: "Query exceeds maximum alias count (alias-bomb protection)",
|
|
8923
|
+
fragment_cycle: "Query contains a cyclic fragment definition"
|
|
8747
8924
|
};
|
|
8748
8925
|
function extractQuery(req) {
|
|
8749
8926
|
const bodyQuery = typeof req.body === "object" && req.body !== null ? req.body.query : void 0;
|
|
@@ -8882,6 +9059,187 @@ function responseSplittingGuard(options = {}) {
|
|
|
8882
9059
|
};
|
|
8883
9060
|
}
|
|
8884
9061
|
|
|
9062
|
+
// src/middleware/correlation.ts
|
|
9063
|
+
var EMPTY_DETECTIONS = Object.freeze({
|
|
9064
|
+
scanner: false,
|
|
9065
|
+
credentialStuffing: false,
|
|
9066
|
+
raceWindow: false,
|
|
9067
|
+
distinctVectors: 0,
|
|
9068
|
+
distinctValues: 0,
|
|
9069
|
+
requestsInWindow: 0
|
|
9070
|
+
});
|
|
9071
|
+
function normalizePair(a, b) {
|
|
9072
|
+
return a < b ? `${a}${b}` : `${b}${a}`;
|
|
9073
|
+
}
|
|
9074
|
+
var CorrelationWindow = class {
|
|
9075
|
+
constructor(options = {}) {
|
|
9076
|
+
// Map iteration order in JS is insertion order, so re-inserting on
|
|
9077
|
+
// access gives us LRU behaviour without a separate linked list.
|
|
9078
|
+
this.buckets = /* @__PURE__ */ new Map();
|
|
9079
|
+
const {
|
|
9080
|
+
windowSeconds = 60,
|
|
9081
|
+
maxIps = 1e4,
|
|
9082
|
+
maxEventsPerIp = 200,
|
|
9083
|
+
scannerDistinctVectors = 3,
|
|
9084
|
+
scannerMinRequests = 20,
|
|
9085
|
+
credentialStuffingDistinctValues = 10,
|
|
9086
|
+
raceWindowMs = 200,
|
|
9087
|
+
racePairs
|
|
9088
|
+
} = options;
|
|
9089
|
+
if (windowSeconds <= 0) throw new Error("windowSeconds must be > 0");
|
|
9090
|
+
if (maxIps < 1) throw new Error("maxIps must be >= 1");
|
|
9091
|
+
if (maxEventsPerIp < 1) throw new Error("maxEventsPerIp must be >= 1");
|
|
9092
|
+
this.windowSeconds = windowSeconds;
|
|
9093
|
+
this.maxIps = maxIps;
|
|
9094
|
+
this.maxEventsPerIp = maxEventsPerIp;
|
|
9095
|
+
this.scannerDistinctVectors = scannerDistinctVectors;
|
|
9096
|
+
this.scannerMinRequests = scannerMinRequests;
|
|
9097
|
+
this.csDistinctValues = credentialStuffingDistinctValues;
|
|
9098
|
+
this.raceWindowSeconds = raceWindowMs / 1e3;
|
|
9099
|
+
this.racePairKeys = /* @__PURE__ */ new Set();
|
|
9100
|
+
this.racePairTuples = [];
|
|
9101
|
+
if (racePairs) {
|
|
9102
|
+
for (const [a, b] of racePairs) {
|
|
9103
|
+
const key = normalizePair(a, b);
|
|
9104
|
+
if (!this.racePairKeys.has(key)) {
|
|
9105
|
+
this.racePairKeys.add(key);
|
|
9106
|
+
const sorted = a < b ? [a, b] : [b, a];
|
|
9107
|
+
this.racePairTuples.push(sorted);
|
|
9108
|
+
}
|
|
9109
|
+
}
|
|
9110
|
+
}
|
|
9111
|
+
}
|
|
9112
|
+
record(ip, vector, route, method = "GET", distinctValue, now) {
|
|
9113
|
+
if (!ip) return EMPTY_DETECTIONS;
|
|
9114
|
+
const ts = now ?? Date.now() / 1e3;
|
|
9115
|
+
const event = {
|
|
9116
|
+
timestamp: ts,
|
|
9117
|
+
vector,
|
|
9118
|
+
route,
|
|
9119
|
+
method,
|
|
9120
|
+
distinctValue
|
|
9121
|
+
};
|
|
9122
|
+
let bucket = this.buckets.get(ip);
|
|
9123
|
+
if (bucket === void 0) {
|
|
9124
|
+
bucket = { events: [] };
|
|
9125
|
+
this.buckets.set(ip, bucket);
|
|
9126
|
+
while (this.buckets.size > this.maxIps) {
|
|
9127
|
+
const oldest = this.buckets.keys().next().value;
|
|
9128
|
+
if (oldest === void 0) break;
|
|
9129
|
+
this.buckets.delete(oldest);
|
|
9130
|
+
}
|
|
9131
|
+
} else {
|
|
9132
|
+
this.buckets.delete(ip);
|
|
9133
|
+
this.buckets.set(ip, bucket);
|
|
9134
|
+
}
|
|
9135
|
+
bucket.events.push(event);
|
|
9136
|
+
this.evictStale(bucket, ts);
|
|
9137
|
+
return this.evaluate(bucket, route);
|
|
9138
|
+
}
|
|
9139
|
+
detectScanner(ip, now) {
|
|
9140
|
+
const bucket = this.buckets.get(ip);
|
|
9141
|
+
if (bucket === void 0) return false;
|
|
9142
|
+
this.evictStale(bucket, now ?? Date.now() / 1e3);
|
|
9143
|
+
return this.isScanner(bucket);
|
|
9144
|
+
}
|
|
9145
|
+
detectCredentialStuffing(ip, route, now) {
|
|
9146
|
+
const bucket = this.buckets.get(ip);
|
|
9147
|
+
if (bucket === void 0) return false;
|
|
9148
|
+
this.evictStale(bucket, now ?? Date.now() / 1e3);
|
|
9149
|
+
return this.isCredentialStuffing(bucket, route);
|
|
9150
|
+
}
|
|
9151
|
+
detectRaceWindow(ip, routePair, now) {
|
|
9152
|
+
const bucket = this.buckets.get(ip);
|
|
9153
|
+
if (bucket === void 0) return false;
|
|
9154
|
+
this.evictStale(bucket, now ?? Date.now() / 1e3);
|
|
9155
|
+
const sorted = routePair[0] < routePair[1] ? routePair : [routePair[1], routePair[0]];
|
|
9156
|
+
return this.racePairInBucket(bucket, sorted);
|
|
9157
|
+
}
|
|
9158
|
+
reset(ip) {
|
|
9159
|
+
if (ip === void 0) {
|
|
9160
|
+
this.buckets.clear();
|
|
9161
|
+
} else {
|
|
9162
|
+
this.buckets.delete(ip);
|
|
9163
|
+
}
|
|
9164
|
+
}
|
|
9165
|
+
stats() {
|
|
9166
|
+
let events = 0;
|
|
9167
|
+
for (const b of this.buckets.values()) events += b.events.length;
|
|
9168
|
+
return { trackedIps: this.buckets.size, eventsInWindow: events };
|
|
9169
|
+
}
|
|
9170
|
+
// -------------------------------------------------------- internals
|
|
9171
|
+
evictStale(bucket, now) {
|
|
9172
|
+
const cutoff = now - this.windowSeconds;
|
|
9173
|
+
let drop = 0;
|
|
9174
|
+
while (drop < bucket.events.length && bucket.events[drop].timestamp < cutoff) {
|
|
9175
|
+
drop++;
|
|
9176
|
+
}
|
|
9177
|
+
if (drop > 0) bucket.events.splice(0, drop);
|
|
9178
|
+
if (bucket.events.length > this.maxEventsPerIp) {
|
|
9179
|
+
bucket.events.splice(0, bucket.events.length - this.maxEventsPerIp);
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
evaluate(bucket, route) {
|
|
9183
|
+
const vectors = /* @__PURE__ */ new Set();
|
|
9184
|
+
const values = /* @__PURE__ */ new Set();
|
|
9185
|
+
for (const e of bucket.events) {
|
|
9186
|
+
vectors.add(e.vector);
|
|
9187
|
+
if (e.route === route && e.distinctValue !== void 0) {
|
|
9188
|
+
values.add(e.distinctValue);
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
return {
|
|
9192
|
+
scanner: this.isScanner(bucket),
|
|
9193
|
+
credentialStuffing: this.isCredentialStuffing(bucket, route),
|
|
9194
|
+
raceWindow: this.isRaceAny(bucket),
|
|
9195
|
+
distinctVectors: vectors.size,
|
|
9196
|
+
distinctValues: values.size,
|
|
9197
|
+
requestsInWindow: bucket.events.length
|
|
9198
|
+
};
|
|
9199
|
+
}
|
|
9200
|
+
isScanner(bucket) {
|
|
9201
|
+
if (bucket.events.length < this.scannerMinRequests) return false;
|
|
9202
|
+
const vectors = /* @__PURE__ */ new Set();
|
|
9203
|
+
for (const e of bucket.events) vectors.add(e.vector);
|
|
9204
|
+
return vectors.size >= this.scannerDistinctVectors;
|
|
9205
|
+
}
|
|
9206
|
+
isCredentialStuffing(bucket, route) {
|
|
9207
|
+
const values = /* @__PURE__ */ new Set();
|
|
9208
|
+
for (const e of bucket.events) {
|
|
9209
|
+
if (e.route === route && e.distinctValue !== void 0) {
|
|
9210
|
+
values.add(e.distinctValue);
|
|
9211
|
+
}
|
|
9212
|
+
}
|
|
9213
|
+
return values.size >= this.csDistinctValues;
|
|
9214
|
+
}
|
|
9215
|
+
racePairInBucket(bucket, sorted) {
|
|
9216
|
+
const [a, b] = sorted;
|
|
9217
|
+
const aTs = [];
|
|
9218
|
+
const bTs = [];
|
|
9219
|
+
for (const e of bucket.events) {
|
|
9220
|
+
if (e.route === a) aTs.push(e.timestamp);
|
|
9221
|
+
else if (e.route === b) bTs.push(e.timestamp);
|
|
9222
|
+
}
|
|
9223
|
+
if (aTs.length === 0 || bTs.length === 0) return false;
|
|
9224
|
+
let ai = 0;
|
|
9225
|
+
let bi = 0;
|
|
9226
|
+
while (ai < aTs.length && bi < bTs.length) {
|
|
9227
|
+
const diff = aTs[ai] - bTs[bi];
|
|
9228
|
+
if (Math.abs(diff) <= this.raceWindowSeconds) return true;
|
|
9229
|
+
if (diff < 0) ai++;
|
|
9230
|
+
else bi++;
|
|
9231
|
+
}
|
|
9232
|
+
return false;
|
|
9233
|
+
}
|
|
9234
|
+
isRaceAny(bucket) {
|
|
9235
|
+
for (const pair of this.racePairTuples) {
|
|
9236
|
+
if (this.racePairInBucket(bucket, pair)) return true;
|
|
9237
|
+
}
|
|
9238
|
+
return false;
|
|
9239
|
+
}
|
|
9240
|
+
};
|
|
9241
|
+
|
|
9242
|
+
exports.CorrelationWindow = CorrelationWindow;
|
|
8885
9243
|
exports.ResponseSplittingError = ResponseSplittingError;
|
|
8886
9244
|
exports.arcis = arcis;
|
|
8887
9245
|
exports.arcisFunction = arcisWithMethods;
|