@arcis/node 1.5.1 → 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/index.mjs
CHANGED
|
@@ -142,7 +142,16 @@ var SQL_PATTERNS = [
|
|
|
142
142
|
/** Time-based blind: PostgreSQL pg_sleep() */
|
|
143
143
|
/\bpg_sleep\s*\(/gi,
|
|
144
144
|
/** Time-based blind: MSSQL WAITFOR DELAY */
|
|
145
|
-
/\bWAITFOR\s+DELAY\b/gi
|
|
145
|
+
/\bWAITFOR\s+DELAY\b/gi,
|
|
146
|
+
/**
|
|
147
|
+
* Oracle DBMS_* stdlib packages used for time-based blind SQLi
|
|
148
|
+
* (DBMS_LOCK.SLEEP, DBMS_PIPE.RECEIVE_MESSAGE) and other Oracle
|
|
149
|
+
* abuse paths. No legitimate user input contains these. Mirrors
|
|
150
|
+
* `sqli-oracle-dbms-packages` in packages/core/patterns.json —
|
|
151
|
+
* improvements.md §1.1.e Q3. Must stay in sync until Node
|
|
152
|
+
* migrates to patterns.json-at-runtime (planned v1.7).
|
|
153
|
+
*/
|
|
154
|
+
/\bDBMS_(?:LOCK|PIPE|UTILITY|XSLPROCESSOR|JAVA|OUTPUT|SCHEDULER)\b/gi
|
|
146
155
|
];
|
|
147
156
|
var PATH_PATTERNS = [
|
|
148
157
|
/** Unix path traversal */
|
|
@@ -180,6 +189,15 @@ var COMMAND_PATTERNS = [
|
|
|
180
189
|
/[;&|`]/g,
|
|
181
190
|
/** Command substitution: $( ... ) — matched as a pair to reduce false positives */
|
|
182
191
|
/\$\(/g,
|
|
192
|
+
/**
|
|
193
|
+
* POSIX shell IFS-substitution: ${IFS} or ${IFS%??}.
|
|
194
|
+
* Attackers use this to inject spaces past metacharacter filters
|
|
195
|
+
* in payloads like `;cat${IFS}/etc/passwd`. Mirrors
|
|
196
|
+
* `cmdi-ifs-bypass` in packages/core/patterns.json — improvements.md
|
|
197
|
+
* §1.1.e Q5. Must stay in sync until Node migrates to
|
|
198
|
+
* patterns.json-at-runtime (planned v1.7).
|
|
199
|
+
*/
|
|
200
|
+
/\$\{IFS(?:%[^}]*)?\}/g,
|
|
183
201
|
/** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
|
|
184
202
|
/%0[0-9a-f]/gi
|
|
185
203
|
];
|
|
@@ -1124,6 +1142,40 @@ function detectHeaderInjection(input) {
|
|
|
1124
1142
|
}
|
|
1125
1143
|
|
|
1126
1144
|
// src/sanitizers/sanitize.ts
|
|
1145
|
+
function multiDecode(value, maxPasses = 4) {
|
|
1146
|
+
for (let i = 0; i < maxPasses; i++) {
|
|
1147
|
+
const prev = value;
|
|
1148
|
+
try {
|
|
1149
|
+
value = decodeURIComponent(value);
|
|
1150
|
+
} catch {
|
|
1151
|
+
}
|
|
1152
|
+
value = htmlEntityDecode(value);
|
|
1153
|
+
if (value === prev) break;
|
|
1154
|
+
}
|
|
1155
|
+
return value;
|
|
1156
|
+
}
|
|
1157
|
+
function htmlEntityDecode(s) {
|
|
1158
|
+
s = s.replace(/&#(\d+);/g, (_m, n) => {
|
|
1159
|
+
const code = parseInt(n, 10);
|
|
1160
|
+
return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
|
|
1161
|
+
});
|
|
1162
|
+
s = s.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => {
|
|
1163
|
+
const code = parseInt(h, 16);
|
|
1164
|
+
return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
|
|
1165
|
+
});
|
|
1166
|
+
const named = {
|
|
1167
|
+
"<": "<",
|
|
1168
|
+
">": ">",
|
|
1169
|
+
"&": "&",
|
|
1170
|
+
""": '"',
|
|
1171
|
+
"'": "'",
|
|
1172
|
+
" ": " "
|
|
1173
|
+
};
|
|
1174
|
+
for (const [entity, ch] of Object.entries(named)) {
|
|
1175
|
+
s = s.split(entity).join(ch);
|
|
1176
|
+
}
|
|
1177
|
+
return s;
|
|
1178
|
+
}
|
|
1127
1179
|
function sanitizeString(value, options = {}) {
|
|
1128
1180
|
if (typeof value !== "string") return value;
|
|
1129
1181
|
const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
|
|
@@ -1131,7 +1183,8 @@ function sanitizeString(value, options = {}) {
|
|
|
1131
1183
|
throw new InputTooLargeError(maxSize, value.length);
|
|
1132
1184
|
}
|
|
1133
1185
|
const reject = options.mode === "reject";
|
|
1134
|
-
let result = value;
|
|
1186
|
+
let result = value.normalize("NFKC");
|
|
1187
|
+
result = multiDecode(result);
|
|
1135
1188
|
if (options.sql !== false) {
|
|
1136
1189
|
if (reject) {
|
|
1137
1190
|
if (detectSql(result)) {
|
|
@@ -1578,7 +1631,9 @@ function encodeForCss(value) {
|
|
|
1578
1631
|
var DEFAULTS = {
|
|
1579
1632
|
maxDepth: 10,
|
|
1580
1633
|
maxLength: 1e4,
|
|
1581
|
-
blockIntrospection: true
|
|
1634
|
+
blockIntrospection: true,
|
|
1635
|
+
maxAliases: 50,
|
|
1636
|
+
blockFragmentCycles: true
|
|
1582
1637
|
};
|
|
1583
1638
|
var INTROSPECTION_PATTERN = /\b__(schema|type|typeKind|directive)\b/;
|
|
1584
1639
|
function computeDepth(query) {
|
|
@@ -1595,22 +1650,87 @@ function computeDepth(query) {
|
|
|
1595
1650
|
}
|
|
1596
1651
|
return max;
|
|
1597
1652
|
}
|
|
1653
|
+
var ALIAS_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
1654
|
+
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;
|
|
1655
|
+
var FRAGMENT_SPREAD_PATTERN = /\.\.\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
|
|
1656
|
+
function countAliases(query) {
|
|
1657
|
+
let n = 0;
|
|
1658
|
+
ALIAS_PATTERN.lastIndex = 0;
|
|
1659
|
+
while (ALIAS_PATTERN.exec(query) !== null) n++;
|
|
1660
|
+
return n;
|
|
1661
|
+
}
|
|
1662
|
+
function hasFragmentCycle(query) {
|
|
1663
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1664
|
+
FRAGMENT_DEF_PATTERN.lastIndex = 0;
|
|
1665
|
+
let match;
|
|
1666
|
+
while ((match = FRAGMENT_DEF_PATTERN.exec(query)) !== null) {
|
|
1667
|
+
const name = match[1];
|
|
1668
|
+
const bodyStart = match.index + match[0].length;
|
|
1669
|
+
let depth = 1;
|
|
1670
|
+
let i = bodyStart;
|
|
1671
|
+
while (i < query.length && depth > 0) {
|
|
1672
|
+
const ch = query[i];
|
|
1673
|
+
if (ch === "{") depth++;
|
|
1674
|
+
else if (ch === "}") depth--;
|
|
1675
|
+
i++;
|
|
1676
|
+
}
|
|
1677
|
+
const bodyEnd = depth === 0 ? i - 1 : i;
|
|
1678
|
+
const body = query.slice(bodyStart, bodyEnd);
|
|
1679
|
+
const spreads = /* @__PURE__ */ new Set();
|
|
1680
|
+
FRAGMENT_SPREAD_PATTERN.lastIndex = 0;
|
|
1681
|
+
let sm;
|
|
1682
|
+
while ((sm = FRAGMENT_SPREAD_PATTERN.exec(body)) !== null) {
|
|
1683
|
+
spreads.add(sm[1]);
|
|
1684
|
+
}
|
|
1685
|
+
deps.set(name, spreads);
|
|
1686
|
+
}
|
|
1687
|
+
if (deps.size === 0) return false;
|
|
1688
|
+
const WHITE = 0;
|
|
1689
|
+
const GRAY = 1;
|
|
1690
|
+
const BLACK = 2;
|
|
1691
|
+
const color = /* @__PURE__ */ new Map();
|
|
1692
|
+
for (const name of deps.keys()) color.set(name, WHITE);
|
|
1693
|
+
function visit(name) {
|
|
1694
|
+
if (color.get(name) === GRAY) return true;
|
|
1695
|
+
if (color.get(name) === BLACK) return false;
|
|
1696
|
+
if (!deps.has(name)) return false;
|
|
1697
|
+
color.set(name, GRAY);
|
|
1698
|
+
for (const child of deps.get(name)) {
|
|
1699
|
+
if (visit(child)) return true;
|
|
1700
|
+
}
|
|
1701
|
+
color.set(name, BLACK);
|
|
1702
|
+
return false;
|
|
1703
|
+
}
|
|
1704
|
+
for (const name of deps.keys()) {
|
|
1705
|
+
if (visit(name)) return true;
|
|
1706
|
+
}
|
|
1707
|
+
return false;
|
|
1708
|
+
}
|
|
1598
1709
|
function inspectGraphqlQuery(query, options = {}) {
|
|
1599
1710
|
const maxDepth = options.maxDepth ?? DEFAULTS.maxDepth;
|
|
1600
1711
|
const maxLength = options.maxLength ?? DEFAULTS.maxLength;
|
|
1601
1712
|
const blockIntrospection = options.blockIntrospection ?? DEFAULTS.blockIntrospection;
|
|
1713
|
+
const maxAliases = options.maxAliases ?? DEFAULTS.maxAliases;
|
|
1714
|
+
const blockFragmentCycles = options.blockFragmentCycles ?? DEFAULTS.blockFragmentCycles;
|
|
1602
1715
|
const length = query.length;
|
|
1603
1716
|
const depth = computeDepth(query);
|
|
1717
|
+
const aliases = countAliases(query);
|
|
1604
1718
|
if (depth > maxDepth) {
|
|
1605
|
-
return { blocked: true, reason: "depth", depth, length };
|
|
1719
|
+
return { blocked: true, reason: "depth", depth, length, aliases };
|
|
1606
1720
|
}
|
|
1607
1721
|
if (blockIntrospection && INTROSPECTION_PATTERN.test(query)) {
|
|
1608
|
-
return { blocked: true, reason: "introspection", depth, length };
|
|
1722
|
+
return { blocked: true, reason: "introspection", depth, length, aliases };
|
|
1723
|
+
}
|
|
1724
|
+
if (aliases > maxAliases) {
|
|
1725
|
+
return { blocked: true, reason: "aliases", depth, length, aliases };
|
|
1726
|
+
}
|
|
1727
|
+
if (blockFragmentCycles && hasFragmentCycle(query)) {
|
|
1728
|
+
return { blocked: true, reason: "fragment_cycle", depth, length, aliases };
|
|
1609
1729
|
}
|
|
1610
1730
|
if (length > maxLength) {
|
|
1611
|
-
return { blocked: true, reason: "length", depth, length };
|
|
1731
|
+
return { blocked: true, reason: "length", depth, length, aliases };
|
|
1612
1732
|
}
|
|
1613
|
-
return { blocked: false, depth, length };
|
|
1733
|
+
return { blocked: false, depth, length, aliases };
|
|
1614
1734
|
}
|
|
1615
1735
|
function detectGraphqlAbuse(query, options) {
|
|
1616
1736
|
if (typeof query !== "string" || query.length === 0) return false;
|
|
@@ -9946,6 +10066,46 @@ function signupProtection(options = {}) {
|
|
|
9946
10066
|
}
|
|
9947
10067
|
|
|
9948
10068
|
// src/middleware/protect.ts
|
|
10069
|
+
function getClientIp(req) {
|
|
10070
|
+
const xff = req?.headers?.["x-forwarded-for"] ?? req?.headers?.["X-Forwarded-For"];
|
|
10071
|
+
if (typeof xff === "string" && xff.length > 0) {
|
|
10072
|
+
const first = xff.split(",")[0]?.trim();
|
|
10073
|
+
if (first) return first;
|
|
10074
|
+
}
|
|
10075
|
+
if (typeof req?.ip === "string") return req.ip;
|
|
10076
|
+
const remote = req?.socket?.remoteAddress;
|
|
10077
|
+
return typeof remote === "string" ? remote : "";
|
|
10078
|
+
}
|
|
10079
|
+
function correlationMiddleware(opts) {
|
|
10080
|
+
const vector = opts.vector ?? "request";
|
|
10081
|
+
const usernameField = opts.usernameField ?? "username";
|
|
10082
|
+
const statusCode = opts.statusCode ?? 429;
|
|
10083
|
+
const message = opts.message ?? "Suspicious request pattern detected.";
|
|
10084
|
+
return function(req, res, next) {
|
|
10085
|
+
const ip = getClientIp(req);
|
|
10086
|
+
if (!ip) return next();
|
|
10087
|
+
const route = opts.route ?? (req.path || req.url || "/");
|
|
10088
|
+
const username = req?.body?.[usernameField];
|
|
10089
|
+
const distinctValue = typeof username === "string" && username.length > 0 ? username : void 0;
|
|
10090
|
+
const detections = opts.window.record(
|
|
10091
|
+
ip,
|
|
10092
|
+
vector,
|
|
10093
|
+
route,
|
|
10094
|
+
req.method || "GET",
|
|
10095
|
+
distinctValue
|
|
10096
|
+
);
|
|
10097
|
+
if (detections.scanner || detections.credentialStuffing || detections.raceWindow) {
|
|
10098
|
+
res.status(statusCode).json({
|
|
10099
|
+
error: message,
|
|
10100
|
+
scanner: detections.scanner,
|
|
10101
|
+
credential_stuffing: detections.credentialStuffing,
|
|
10102
|
+
race_window: detections.raceWindow
|
|
10103
|
+
});
|
|
10104
|
+
return;
|
|
10105
|
+
}
|
|
10106
|
+
next();
|
|
10107
|
+
};
|
|
10108
|
+
}
|
|
9949
10109
|
function resolve(override, defaults) {
|
|
9950
10110
|
if (override === false) return null;
|
|
9951
10111
|
if (override === void 0) return defaults;
|
|
@@ -9965,6 +10125,11 @@ function protectLogin(options = {}) {
|
|
|
9965
10125
|
if (csrf) middlewares.push(csrfProtection(csrf));
|
|
9966
10126
|
const sanitize = resolve(options.sanitize, {});
|
|
9967
10127
|
if (sanitize) middlewares.push(createSanitizer(sanitize));
|
|
10128
|
+
if (options.correlation) {
|
|
10129
|
+
middlewares.push(
|
|
10130
|
+
correlationMiddleware({ vector: "login", ...options.correlation })
|
|
10131
|
+
);
|
|
10132
|
+
}
|
|
9968
10133
|
return middlewares;
|
|
9969
10134
|
}
|
|
9970
10135
|
function protectSignup(options = {}) {
|
|
@@ -9981,6 +10146,11 @@ function protectSignup(options = {}) {
|
|
|
9981
10146
|
if (sanitize) middlewares.push(createSanitizer(sanitize));
|
|
9982
10147
|
const signup = resolve(options.signup, {});
|
|
9983
10148
|
if (signup) middlewares.push(signupProtection(signup));
|
|
10149
|
+
if (options.correlation) {
|
|
10150
|
+
middlewares.push(
|
|
10151
|
+
correlationMiddleware({ vector: "signup", ...options.correlation })
|
|
10152
|
+
);
|
|
10153
|
+
}
|
|
9984
10154
|
return middlewares;
|
|
9985
10155
|
}
|
|
9986
10156
|
function protectApi(options = {}) {
|
|
@@ -9991,6 +10161,11 @@ function protectApi(options = {}) {
|
|
|
9991
10161
|
if (cors) middlewares.push(safeCors(cors));
|
|
9992
10162
|
const sanitize = resolve(options.sanitize, {});
|
|
9993
10163
|
if (sanitize) middlewares.push(createSanitizer(sanitize));
|
|
10164
|
+
if (options.correlation) {
|
|
10165
|
+
middlewares.push(
|
|
10166
|
+
correlationMiddleware({ vector: "api", ...options.correlation })
|
|
10167
|
+
);
|
|
10168
|
+
}
|
|
9994
10169
|
return middlewares;
|
|
9995
10170
|
}
|
|
9996
10171
|
|
|
@@ -9998,7 +10173,9 @@ function protectApi(options = {}) {
|
|
|
9998
10173
|
var DEFAULT_MESSAGES = {
|
|
9999
10174
|
depth: "Query exceeds maximum nesting depth",
|
|
10000
10175
|
length: "Query exceeds maximum length",
|
|
10001
|
-
introspection: "Introspection queries are disabled"
|
|
10176
|
+
introspection: "Introspection queries are disabled",
|
|
10177
|
+
aliases: "Query exceeds maximum alias count (alias-bomb protection)",
|
|
10178
|
+
fragment_cycle: "Query contains a cyclic fragment definition"
|
|
10002
10179
|
};
|
|
10003
10180
|
function extractQuery(req) {
|
|
10004
10181
|
const bodyQuery = typeof req.body === "object" && req.body !== null ? req.body.query : void 0;
|
|
@@ -10034,6 +10211,186 @@ function graphqlGuard(options = {}) {
|
|
|
10034
10211
|
};
|
|
10035
10212
|
}
|
|
10036
10213
|
|
|
10214
|
+
// src/middleware/correlation.ts
|
|
10215
|
+
var EMPTY_DETECTIONS = Object.freeze({
|
|
10216
|
+
scanner: false,
|
|
10217
|
+
credentialStuffing: false,
|
|
10218
|
+
raceWindow: false,
|
|
10219
|
+
distinctVectors: 0,
|
|
10220
|
+
distinctValues: 0,
|
|
10221
|
+
requestsInWindow: 0
|
|
10222
|
+
});
|
|
10223
|
+
function normalizePair(a, b) {
|
|
10224
|
+
return a < b ? `${a}${b}` : `${b}${a}`;
|
|
10225
|
+
}
|
|
10226
|
+
var CorrelationWindow = class {
|
|
10227
|
+
constructor(options = {}) {
|
|
10228
|
+
// Map iteration order in JS is insertion order, so re-inserting on
|
|
10229
|
+
// access gives us LRU behaviour without a separate linked list.
|
|
10230
|
+
this.buckets = /* @__PURE__ */ new Map();
|
|
10231
|
+
const {
|
|
10232
|
+
windowSeconds = 60,
|
|
10233
|
+
maxIps = 1e4,
|
|
10234
|
+
maxEventsPerIp = 200,
|
|
10235
|
+
scannerDistinctVectors = 3,
|
|
10236
|
+
scannerMinRequests = 20,
|
|
10237
|
+
credentialStuffingDistinctValues = 10,
|
|
10238
|
+
raceWindowMs = 200,
|
|
10239
|
+
racePairs
|
|
10240
|
+
} = options;
|
|
10241
|
+
if (windowSeconds <= 0) throw new Error("windowSeconds must be > 0");
|
|
10242
|
+
if (maxIps < 1) throw new Error("maxIps must be >= 1");
|
|
10243
|
+
if (maxEventsPerIp < 1) throw new Error("maxEventsPerIp must be >= 1");
|
|
10244
|
+
this.windowSeconds = windowSeconds;
|
|
10245
|
+
this.maxIps = maxIps;
|
|
10246
|
+
this.maxEventsPerIp = maxEventsPerIp;
|
|
10247
|
+
this.scannerDistinctVectors = scannerDistinctVectors;
|
|
10248
|
+
this.scannerMinRequests = scannerMinRequests;
|
|
10249
|
+
this.csDistinctValues = credentialStuffingDistinctValues;
|
|
10250
|
+
this.raceWindowSeconds = raceWindowMs / 1e3;
|
|
10251
|
+
this.racePairKeys = /* @__PURE__ */ new Set();
|
|
10252
|
+
this.racePairTuples = [];
|
|
10253
|
+
if (racePairs) {
|
|
10254
|
+
for (const [a, b] of racePairs) {
|
|
10255
|
+
const key = normalizePair(a, b);
|
|
10256
|
+
if (!this.racePairKeys.has(key)) {
|
|
10257
|
+
this.racePairKeys.add(key);
|
|
10258
|
+
const sorted = a < b ? [a, b] : [b, a];
|
|
10259
|
+
this.racePairTuples.push(sorted);
|
|
10260
|
+
}
|
|
10261
|
+
}
|
|
10262
|
+
}
|
|
10263
|
+
}
|
|
10264
|
+
record(ip, vector, route, method = "GET", distinctValue, now) {
|
|
10265
|
+
if (!ip) return EMPTY_DETECTIONS;
|
|
10266
|
+
const ts = now ?? Date.now() / 1e3;
|
|
10267
|
+
const event = {
|
|
10268
|
+
timestamp: ts,
|
|
10269
|
+
vector,
|
|
10270
|
+
route,
|
|
10271
|
+
method,
|
|
10272
|
+
distinctValue
|
|
10273
|
+
};
|
|
10274
|
+
let bucket = this.buckets.get(ip);
|
|
10275
|
+
if (bucket === void 0) {
|
|
10276
|
+
bucket = { events: [] };
|
|
10277
|
+
this.buckets.set(ip, bucket);
|
|
10278
|
+
while (this.buckets.size > this.maxIps) {
|
|
10279
|
+
const oldest = this.buckets.keys().next().value;
|
|
10280
|
+
if (oldest === void 0) break;
|
|
10281
|
+
this.buckets.delete(oldest);
|
|
10282
|
+
}
|
|
10283
|
+
} else {
|
|
10284
|
+
this.buckets.delete(ip);
|
|
10285
|
+
this.buckets.set(ip, bucket);
|
|
10286
|
+
}
|
|
10287
|
+
bucket.events.push(event);
|
|
10288
|
+
this.evictStale(bucket, ts);
|
|
10289
|
+
return this.evaluate(bucket, route);
|
|
10290
|
+
}
|
|
10291
|
+
detectScanner(ip, now) {
|
|
10292
|
+
const bucket = this.buckets.get(ip);
|
|
10293
|
+
if (bucket === void 0) return false;
|
|
10294
|
+
this.evictStale(bucket, now ?? Date.now() / 1e3);
|
|
10295
|
+
return this.isScanner(bucket);
|
|
10296
|
+
}
|
|
10297
|
+
detectCredentialStuffing(ip, route, now) {
|
|
10298
|
+
const bucket = this.buckets.get(ip);
|
|
10299
|
+
if (bucket === void 0) return false;
|
|
10300
|
+
this.evictStale(bucket, now ?? Date.now() / 1e3);
|
|
10301
|
+
return this.isCredentialStuffing(bucket, route);
|
|
10302
|
+
}
|
|
10303
|
+
detectRaceWindow(ip, routePair, now) {
|
|
10304
|
+
const bucket = this.buckets.get(ip);
|
|
10305
|
+
if (bucket === void 0) return false;
|
|
10306
|
+
this.evictStale(bucket, now ?? Date.now() / 1e3);
|
|
10307
|
+
const sorted = routePair[0] < routePair[1] ? routePair : [routePair[1], routePair[0]];
|
|
10308
|
+
return this.racePairInBucket(bucket, sorted);
|
|
10309
|
+
}
|
|
10310
|
+
reset(ip) {
|
|
10311
|
+
if (ip === void 0) {
|
|
10312
|
+
this.buckets.clear();
|
|
10313
|
+
} else {
|
|
10314
|
+
this.buckets.delete(ip);
|
|
10315
|
+
}
|
|
10316
|
+
}
|
|
10317
|
+
stats() {
|
|
10318
|
+
let events = 0;
|
|
10319
|
+
for (const b of this.buckets.values()) events += b.events.length;
|
|
10320
|
+
return { trackedIps: this.buckets.size, eventsInWindow: events };
|
|
10321
|
+
}
|
|
10322
|
+
// -------------------------------------------------------- internals
|
|
10323
|
+
evictStale(bucket, now) {
|
|
10324
|
+
const cutoff = now - this.windowSeconds;
|
|
10325
|
+
let drop = 0;
|
|
10326
|
+
while (drop < bucket.events.length && bucket.events[drop].timestamp < cutoff) {
|
|
10327
|
+
drop++;
|
|
10328
|
+
}
|
|
10329
|
+
if (drop > 0) bucket.events.splice(0, drop);
|
|
10330
|
+
if (bucket.events.length > this.maxEventsPerIp) {
|
|
10331
|
+
bucket.events.splice(0, bucket.events.length - this.maxEventsPerIp);
|
|
10332
|
+
}
|
|
10333
|
+
}
|
|
10334
|
+
evaluate(bucket, route) {
|
|
10335
|
+
const vectors = /* @__PURE__ */ new Set();
|
|
10336
|
+
const values = /* @__PURE__ */ new Set();
|
|
10337
|
+
for (const e of bucket.events) {
|
|
10338
|
+
vectors.add(e.vector);
|
|
10339
|
+
if (e.route === route && e.distinctValue !== void 0) {
|
|
10340
|
+
values.add(e.distinctValue);
|
|
10341
|
+
}
|
|
10342
|
+
}
|
|
10343
|
+
return {
|
|
10344
|
+
scanner: this.isScanner(bucket),
|
|
10345
|
+
credentialStuffing: this.isCredentialStuffing(bucket, route),
|
|
10346
|
+
raceWindow: this.isRaceAny(bucket),
|
|
10347
|
+
distinctVectors: vectors.size,
|
|
10348
|
+
distinctValues: values.size,
|
|
10349
|
+
requestsInWindow: bucket.events.length
|
|
10350
|
+
};
|
|
10351
|
+
}
|
|
10352
|
+
isScanner(bucket) {
|
|
10353
|
+
if (bucket.events.length < this.scannerMinRequests) return false;
|
|
10354
|
+
const vectors = /* @__PURE__ */ new Set();
|
|
10355
|
+
for (const e of bucket.events) vectors.add(e.vector);
|
|
10356
|
+
return vectors.size >= this.scannerDistinctVectors;
|
|
10357
|
+
}
|
|
10358
|
+
isCredentialStuffing(bucket, route) {
|
|
10359
|
+
const values = /* @__PURE__ */ new Set();
|
|
10360
|
+
for (const e of bucket.events) {
|
|
10361
|
+
if (e.route === route && e.distinctValue !== void 0) {
|
|
10362
|
+
values.add(e.distinctValue);
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
10365
|
+
return values.size >= this.csDistinctValues;
|
|
10366
|
+
}
|
|
10367
|
+
racePairInBucket(bucket, sorted) {
|
|
10368
|
+
const [a, b] = sorted;
|
|
10369
|
+
const aTs = [];
|
|
10370
|
+
const bTs = [];
|
|
10371
|
+
for (const e of bucket.events) {
|
|
10372
|
+
if (e.route === a) aTs.push(e.timestamp);
|
|
10373
|
+
else if (e.route === b) bTs.push(e.timestamp);
|
|
10374
|
+
}
|
|
10375
|
+
if (aTs.length === 0 || bTs.length === 0) return false;
|
|
10376
|
+
let ai = 0;
|
|
10377
|
+
let bi = 0;
|
|
10378
|
+
while (ai < aTs.length && bi < bTs.length) {
|
|
10379
|
+
const diff = aTs[ai] - bTs[bi];
|
|
10380
|
+
if (Math.abs(diff) <= this.raceWindowSeconds) return true;
|
|
10381
|
+
if (diff < 0) ai++;
|
|
10382
|
+
else bi++;
|
|
10383
|
+
}
|
|
10384
|
+
return false;
|
|
10385
|
+
}
|
|
10386
|
+
isRaceAny(bucket) {
|
|
10387
|
+
for (const pair of this.racePairTuples) {
|
|
10388
|
+
if (this.racePairInBucket(bucket, pair)) return true;
|
|
10389
|
+
}
|
|
10390
|
+
return false;
|
|
10391
|
+
}
|
|
10392
|
+
};
|
|
10393
|
+
|
|
10037
10394
|
// src/sanitizers/prompt-injection.ts
|
|
10038
10395
|
var SIGNATURES = [
|
|
10039
10396
|
// --- HIGH severity: clear override / jailbreak attempts ---
|
|
@@ -10163,6 +10520,47 @@ var SIGNATURES = [
|
|
|
10163
10520
|
severity: "medium",
|
|
10164
10521
|
description: "ROT13 / Caesar-cipher decode hint"
|
|
10165
10522
|
},
|
|
10523
|
+
// ── V32: AI agent toolcall injection (improvements.md §1.2) ────────
|
|
10524
|
+
// Modern LLM agents (Claude tool-use, OpenAI function-calling,
|
|
10525
|
+
// ReAct loops) read tool definitions from the system prompt and
|
|
10526
|
+
// JSON-shaped requests from the model. A malicious user can embed
|
|
10527
|
+
// those shapes in their input to make the host think they invoked
|
|
10528
|
+
// a tool, or to trick the model into echoing a synthesized
|
|
10529
|
+
// tool_call that the runtime then executes.
|
|
10530
|
+
//
|
|
10531
|
+
// Narrow patterns — match the literal JSON keys and inline
|
|
10532
|
+
// tool-name shapes. Won't false-positive on plain English text
|
|
10533
|
+
// discussing tools.
|
|
10534
|
+
{
|
|
10535
|
+
rule: "agent-toolcall-marker",
|
|
10536
|
+
pattern: /"(?:tool_call|function_call|call_tool|tool_use|toolUse)"\s*:\s*\{/i,
|
|
10537
|
+
severity: "high",
|
|
10538
|
+
description: 'Injected agent tool-call JSON shape (e.g. {"tool_call":{...}})'
|
|
10539
|
+
},
|
|
10540
|
+
{
|
|
10541
|
+
rule: "agent-tool-name-spoof",
|
|
10542
|
+
pattern: /"name"\s*:\s*"(?:exec|shell|run_command|system|bash|cmd|python|eval|read_file|write_file|delete_file)"/i,
|
|
10543
|
+
severity: "high",
|
|
10544
|
+
description: "Forged tool-name attempting privileged tool invocation"
|
|
10545
|
+
},
|
|
10546
|
+
{
|
|
10547
|
+
rule: "agent-tool-result-marker",
|
|
10548
|
+
pattern: /"(?:tool_result|function_result|tool_output)"\s*:\s*[\{\["]/i,
|
|
10549
|
+
severity: "high",
|
|
10550
|
+
description: "Injected fake tool-result block (trick agent into trusting fabricated output)"
|
|
10551
|
+
},
|
|
10552
|
+
{
|
|
10553
|
+
rule: "ansi-escape-sequence",
|
|
10554
|
+
pattern: /\x1b\[/,
|
|
10555
|
+
severity: "medium",
|
|
10556
|
+
description: "ANSI escape sequence (terminal hijack / output spoofing on CLI agents)"
|
|
10557
|
+
},
|
|
10558
|
+
{
|
|
10559
|
+
rule: "claude-tool-use-tags",
|
|
10560
|
+
pattern: /<\/?\s*(?:tool_use|tool_result|invoke|function_calls?|parameter)\b/i,
|
|
10561
|
+
severity: "high",
|
|
10562
|
+
description: "Claude/OpenAI tool-use XML-style tag forgery"
|
|
10563
|
+
},
|
|
10166
10564
|
// --- LOW severity: ambiguous but worth flagging in strict mode ---
|
|
10167
10565
|
{
|
|
10168
10566
|
rule: "from-now-on",
|
|
@@ -10716,6 +11114,6 @@ function createRedisStore(options) {
|
|
|
10716
11114
|
return new RedisStore(options);
|
|
10717
11115
|
}
|
|
10718
11116
|
|
|
10719
|
-
export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, ERRORS, Guards, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, ResponseSplittingError, SanitizationError, SecurityThreatError, TelemetryClient, TelemetryHttpError, VALIDATION, arcis, arcisWithMethods as arcisFunction, botProtection, checkSignup, createCors, createCsrf, createErrorHandler, createHeaders, createHpp, createRateLimiter, createRedactor, createRedisStore, createSafeLogger, createSanitizer, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, createValidator, csrfProtection, main_default as default, detectBot, detectClientIp, detectCommandInjection, detectGraphqlAbuse, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPromptInjection, detectPrototypePollution, detectResponseSplitting, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, eventLoopProtection, fingerprint, formatDuration, generateCsrfToken, graphqlGuard, hpp, inspectGraphqlQuery, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, massAssign, methodAllowlist, parseDuration, pinnedDnsLookup, protectApi, protectLogin, protectSignup, rateLimit, redactObjectPii, redactPii, responseSplittingGuard, safeCors, safeFollowRedirect, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizePromptInjection, sanitizeResponseHeader, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, signupProtection, tokenBudget, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, validateUrlAsync, verifyEmailMx };
|
|
11117
|
+
export { ArcisError, ValidationError as ArcisValidationError, BLOCKED, CorrelationWindow, ERRORS, Guards, HEADERS, INPUT, InputTooLargeError, MemoryStore, RATE_LIMIT, REDACTION, RateLimitError, RedisStore, ResponseSplittingError, SanitizationError, SecurityThreatError, TelemetryClient, TelemetryHttpError, VALIDATION, arcis, arcisWithMethods as arcisFunction, botProtection, checkSignup, createCors, createCsrf, createErrorHandler, createHeaders, createHpp, createRateLimiter, createRedactor, createRedisStore, createSafeLogger, createSanitizer, createSecureCookies, createSlidingWindowLimiter, createTokenBucketLimiter, createValidator, csrfProtection, main_default as default, detectBot, detectClientIp, detectCommandInjection, detectGraphqlAbuse, detectHeaderInjection, detectJsonpInjection, detectNoSqlInjection, detectPathTraversal, detectPii, detectPromptInjection, detectPrototypePollution, detectResponseSplitting, detectSql, detectSsti, detectXss, detectXxe, encodeForAttribute, encodeForCss, encodeForHtml, encodeForJs, encodeForUrl, enforceSecureCookie, errorHandler, eventLoopProtection, fingerprint, formatDuration, generateCsrfToken, graphqlGuard, hpp, inspectGraphqlQuery, isDangerousExtension, isDangerousNoSqlKey, isDangerousProtoKey, isPrivateIp, isRedirectSafe, isUrlSafe, isValidEmailSyntax, massAssign, methodAllowlist, parseDuration, pinnedDnsLookup, protectApi, protectLogin, protectSignup, rateLimit, redactObjectPii, redactPii, responseSplittingGuard, safeCors, safeFollowRedirect, safeLog, sanitizeCommand, sanitizeFilename, sanitizeHeaderValue, sanitizeHeaders, sanitizeJsonpCallback, sanitizeObject, sanitizePath, sanitizePromptInjection, sanitizeResponseHeader, sanitizeSql, sanitizeSsti, sanitizeString, sanitizeXss, sanitizeXxe, scanObjectPii, scanPii, secureCookieDefaults, securityHeaders, signupProtection, tokenBudget, validate, validateCsrfToken, validateEmail, validateFile, validateRedirect, validateUrl, validateUrlAsync, verifyEmailMx };
|
|
10720
11118
|
//# sourceMappingURL=index.mjs.map
|
|
10721
11119
|
//# sourceMappingURL=index.mjs.map
|