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