@anomira/node-sdk 0.2.6 → 0.2.7
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/dist/index.cjs +370 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +370 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -399,6 +399,8 @@ var EventName = {
|
|
|
399
399
|
RATE_LIMIT: "http.ratelimit.exceeded",
|
|
400
400
|
XSS_DETECTED: "http.xss.detected",
|
|
401
401
|
PATH_TRAVERSAL: "http.path.traversal",
|
|
402
|
+
SSRF_ATTEMPT: "http.ssrf.attempt",
|
|
403
|
+
JWT_MANIPULATION: "http.jwt.manipulation",
|
|
402
404
|
SCAN_DETECTED: "http.scan.detected",
|
|
403
405
|
IDOR_ATTEMPT: "user.idor.attempt",
|
|
404
406
|
SQL_ERROR: "db.sql.error",
|
|
@@ -622,6 +624,261 @@ function str(v) {
|
|
|
622
624
|
if (v === void 0) return "";
|
|
623
625
|
return Array.isArray(v) ? v[0] ?? "" : v;
|
|
624
626
|
}
|
|
627
|
+
|
|
628
|
+
// src/ssrf.ts
|
|
629
|
+
var URL_PARAM_PATTERN = /^(url|uri|path|link|next|target|src|href|redirect|redirecturl|returnurl|successurl|callback|fetch|image|imageurl|avatar|photo|webhook|endpoint|to|host|domain|site|page|file|load|open|download|import|include|embed|source|proxy|destination|dest|resource|location|goto|return|referer|origin|remote|ping|pull)$/i;
|
|
630
|
+
var DANGEROUS_SCHEMES = /* @__PURE__ */ new Set([
|
|
631
|
+
"file",
|
|
632
|
+
"gopher",
|
|
633
|
+
"dict",
|
|
634
|
+
"ftp",
|
|
635
|
+
"sftp",
|
|
636
|
+
"ldap",
|
|
637
|
+
"ldaps",
|
|
638
|
+
"netdoc",
|
|
639
|
+
"jar",
|
|
640
|
+
"mailto",
|
|
641
|
+
"telnet",
|
|
642
|
+
"tftp",
|
|
643
|
+
"finger"
|
|
644
|
+
]);
|
|
645
|
+
function intToIp(n) {
|
|
646
|
+
return [
|
|
647
|
+
n >>> 24 & 255,
|
|
648
|
+
n >>> 16 & 255,
|
|
649
|
+
n >>> 8 & 255,
|
|
650
|
+
n & 255
|
|
651
|
+
].join(".");
|
|
652
|
+
}
|
|
653
|
+
function parseOctet(s) {
|
|
654
|
+
s = s.trim();
|
|
655
|
+
if (/^0x[0-9a-fA-F]+$/.test(s)) return parseInt(s, 16);
|
|
656
|
+
if (/^0[0-9]+$/.test(s)) return parseInt(s, 8);
|
|
657
|
+
if (/^[0-9]+$/.test(s)) return parseInt(s, 10);
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
function normalizeIp(hostname) {
|
|
661
|
+
const h = hostname.trim().toLowerCase();
|
|
662
|
+
const v6mapped = h.match(/^(?:::ffff:)([0-9a-f]{1,4}:[0-9a-f]{1,4})$/i) ?? h.match(/^(?:0{0,4}:){5}ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
|
663
|
+
if (v6mapped?.[1]) {
|
|
664
|
+
const parts2 = v6mapped[1].split(":");
|
|
665
|
+
if (parts2.length === 2) {
|
|
666
|
+
const hi = parseInt(parts2[0], 16);
|
|
667
|
+
const lo = parseInt(parts2[1], 16);
|
|
668
|
+
if (!isNaN(hi) && !isNaN(lo)) return intToIp(hi << 16 | lo);
|
|
669
|
+
}
|
|
670
|
+
return normalizeIp(v6mapped[1]);
|
|
671
|
+
}
|
|
672
|
+
if (h === "::1" || h === "0:0:0:0:0:0:0:1") return "127.0.0.1";
|
|
673
|
+
if (/^0x[0-9a-f]+$/i.test(h)) {
|
|
674
|
+
const n = parseInt(h, 16);
|
|
675
|
+
if (!isNaN(n) && n >= 0 && n <= 4294967295) return intToIp(n);
|
|
676
|
+
}
|
|
677
|
+
if (/^\d+$/.test(h)) {
|
|
678
|
+
const n = parseInt(h, 10);
|
|
679
|
+
if (!isNaN(n) && n >= 0 && n <= 4294967295) return intToIp(n);
|
|
680
|
+
}
|
|
681
|
+
const parts = h.split(".");
|
|
682
|
+
if (parts.length >= 1 && parts.length <= 4) {
|
|
683
|
+
const octets = [];
|
|
684
|
+
for (const p of parts) {
|
|
685
|
+
const v = parseOctet(p);
|
|
686
|
+
if (v === null || v < 0 || v > 255) break;
|
|
687
|
+
octets.push(v);
|
|
688
|
+
}
|
|
689
|
+
if (octets.length === 4) return octets.join(".");
|
|
690
|
+
if (octets.length === 2) return `${octets[0]}.0.0.${octets[1]}`;
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
function ipToInt(ip) {
|
|
695
|
+
const parts = ip.split(".").map(Number);
|
|
696
|
+
return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
|
|
697
|
+
}
|
|
698
|
+
var PRIVATE_RANGES2 = [
|
|
699
|
+
{ start: ipToInt("0.0.0.0"), end: ipToInt("0.255.255.255"), label: "unspecified" },
|
|
700
|
+
{ start: ipToInt("10.0.0.0"), end: ipToInt("10.255.255.255"), label: "private-10" },
|
|
701
|
+
{ start: ipToInt("100.64.0.0"), end: ipToInt("100.127.255.255"), label: "carrier-nat" },
|
|
702
|
+
{ start: ipToInt("127.0.0.0"), end: ipToInt("127.255.255.255"), label: "loopback" },
|
|
703
|
+
{ start: ipToInt("169.254.0.0"), end: ipToInt("169.254.255.255"), label: "link-local" },
|
|
704
|
+
// AWS/GCP metadata lives here
|
|
705
|
+
{ start: ipToInt("172.16.0.0"), end: ipToInt("172.31.255.255"), label: "private-172" },
|
|
706
|
+
{ start: ipToInt("192.0.0.0"), end: ipToInt("192.0.0.255"), label: "iana-special" },
|
|
707
|
+
// Oracle Cloud metadata
|
|
708
|
+
{ start: ipToInt("192.168.0.0"), end: ipToInt("192.168.255.255"), label: "private-192" },
|
|
709
|
+
{ start: ipToInt("198.18.0.0"), end: ipToInt("198.19.255.255"), label: "benchmark" },
|
|
710
|
+
{ start: ipToInt("240.0.0.0"), end: ipToInt("255.255.255.255"), label: "reserved" },
|
|
711
|
+
// Specific cloud metadata endpoints not covered by ranges above
|
|
712
|
+
{ start: ipToInt("100.100.100.200"), end: ipToInt("100.100.100.200"), label: "alibaba-metadata" },
|
|
713
|
+
{ start: ipToInt("168.63.129.16"), end: ipToInt("168.63.129.16"), label: "azure-metadata" }
|
|
714
|
+
];
|
|
715
|
+
function isPrivateIp2(ip) {
|
|
716
|
+
const n = ipToInt(ip);
|
|
717
|
+
for (const range of PRIVATE_RANGES2) {
|
|
718
|
+
if (n >= range.start && n <= range.end) return { private: true, label: range.label };
|
|
719
|
+
}
|
|
720
|
+
return { private: false, label: "" };
|
|
721
|
+
}
|
|
722
|
+
var INTERNAL_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
723
|
+
"localhost",
|
|
724
|
+
"local",
|
|
725
|
+
"localdomain",
|
|
726
|
+
"metadata",
|
|
727
|
+
"metadata.google.internal",
|
|
728
|
+
"169.254.169.254",
|
|
729
|
+
// canonical — also caught by range check
|
|
730
|
+
"instance-data"
|
|
731
|
+
// AWS internal alias
|
|
732
|
+
]);
|
|
733
|
+
function checkUrl(rawUrl, fieldName) {
|
|
734
|
+
if (!rawUrl.includes("://") && !rawUrl.startsWith("//")) return null;
|
|
735
|
+
let parsed;
|
|
736
|
+
try {
|
|
737
|
+
const withScheme = rawUrl.startsWith("//") ? `https:${rawUrl}` : rawUrl;
|
|
738
|
+
parsed = new URL(withScheme);
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
const scheme = parsed.protocol.replace(":", "").toLowerCase();
|
|
743
|
+
const hostname = parsed.hostname.toLowerCase().replace(/\[|\]/g, "");
|
|
744
|
+
if (DANGEROUS_SCHEMES.has(scheme)) {
|
|
745
|
+
return { detected: true, payload: rawUrl, field: fieldName, reason: `dangerous-scheme:${scheme}` };
|
|
746
|
+
}
|
|
747
|
+
if (scheme !== "http" && scheme !== "https") return null;
|
|
748
|
+
if (INTERNAL_HOSTNAMES.has(hostname)) {
|
|
749
|
+
return { detected: true, payload: rawUrl, field: fieldName, reason: `internal-hostname:${hostname}` };
|
|
750
|
+
}
|
|
751
|
+
const normalized = normalizeIp(hostname);
|
|
752
|
+
if (normalized) {
|
|
753
|
+
const { private: isPrivate, label } = isPrivateIp2(normalized);
|
|
754
|
+
if (isPrivate) {
|
|
755
|
+
return { detected: true, payload: rawUrl, field: fieldName, reason: `private-ip:${label}` };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
function scanForSsrf(body, query) {
|
|
761
|
+
const candidates = [];
|
|
762
|
+
for (const [key, val] of Object.entries(query)) {
|
|
763
|
+
if (!URL_PARAM_PATTERN.test(key)) continue;
|
|
764
|
+
const v = Array.isArray(val) ? val[0] : val;
|
|
765
|
+
if (typeof v === "string" && v.length > 0) candidates.push({ name: key, value: v });
|
|
766
|
+
}
|
|
767
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
768
|
+
const flat = body;
|
|
769
|
+
for (const [key, val] of Object.entries(flat)) {
|
|
770
|
+
if (!URL_PARAM_PATTERN.test(key)) continue;
|
|
771
|
+
if (typeof val === "string" && val.length > 0) candidates.push({ name: key, value: val });
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
for (const { name, value } of candidates) {
|
|
775
|
+
const signal = checkUrl(value, name);
|
|
776
|
+
if (signal) return signal;
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/jwt-detect.ts
|
|
782
|
+
var STANDARD_ALGORITHMS = /* @__PURE__ */ new Set([
|
|
783
|
+
// HMAC (symmetric)
|
|
784
|
+
"HS256",
|
|
785
|
+
"HS384",
|
|
786
|
+
"HS512",
|
|
787
|
+
// RSA PKCS#1 (asymmetric)
|
|
788
|
+
"RS256",
|
|
789
|
+
"RS384",
|
|
790
|
+
"RS512",
|
|
791
|
+
// ECDSA (asymmetric)
|
|
792
|
+
"ES256",
|
|
793
|
+
"ES384",
|
|
794
|
+
"ES512",
|
|
795
|
+
// RSA-PSS (asymmetric)
|
|
796
|
+
"PS256",
|
|
797
|
+
"PS384",
|
|
798
|
+
"PS512",
|
|
799
|
+
// Edwards-curve (RFC 8037)
|
|
800
|
+
"EdDSA"
|
|
801
|
+
]);
|
|
802
|
+
var HMAC_ALGORITHMS = /* @__PURE__ */ new Set(["HS256", "HS384", "HS512"]);
|
|
803
|
+
var MAX_HMAC_SIG_LENGTH = 128;
|
|
804
|
+
function decodeBase64Url(s) {
|
|
805
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
806
|
+
const pad = (4 - padded.length % 4) % 4;
|
|
807
|
+
return Buffer.from(padded + "=".repeat(pad), "base64").toString("utf8");
|
|
808
|
+
}
|
|
809
|
+
function analyseJwt(token) {
|
|
810
|
+
const clean = token.trim();
|
|
811
|
+
const parts = clean.split(".");
|
|
812
|
+
if (parts.length < 3 || parts[2] === "") {
|
|
813
|
+
return {
|
|
814
|
+
detected: true,
|
|
815
|
+
attack: "missing_signature",
|
|
816
|
+
alg: null,
|
|
817
|
+
detail: `JWT has ${parts.length} segment(s) \u2014 signature is missing or empty.`
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
let header;
|
|
821
|
+
try {
|
|
822
|
+
header = JSON.parse(decodeBase64Url(parts[0]));
|
|
823
|
+
} catch {
|
|
824
|
+
return { detected: false, attack: null, alg: null, detail: "" };
|
|
825
|
+
}
|
|
826
|
+
const alg = typeof header["alg"] === "string" ? header["alg"] : null;
|
|
827
|
+
if (!alg) {
|
|
828
|
+
return { detected: false, attack: null, alg: null, detail: "" };
|
|
829
|
+
}
|
|
830
|
+
if (alg.toLowerCase() === "none") {
|
|
831
|
+
return {
|
|
832
|
+
detected: true,
|
|
833
|
+
attack: "alg_none",
|
|
834
|
+
alg,
|
|
835
|
+
detail: `JWT header specifies alg="${alg}" \u2014 server told to skip signature verification.`
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
if (!STANDARD_ALGORITHMS.has(alg)) {
|
|
839
|
+
return {
|
|
840
|
+
detected: true,
|
|
841
|
+
attack: "unknown_algorithm",
|
|
842
|
+
alg,
|
|
843
|
+
detail: `JWT header specifies non-standard alg="${alg}" \u2014 not in RFC 7518 algorithm set.`
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
if (HMAC_ALGORITHMS.has(alg) && parts[2].length > MAX_HMAC_SIG_LENGTH) {
|
|
847
|
+
return {
|
|
848
|
+
detected: true,
|
|
849
|
+
attack: "algorithm_confusion",
|
|
850
|
+
alg,
|
|
851
|
+
detail: `JWT claims ${alg} (HMAC) but signature is ${parts[2].length} chars \u2014 characteristic of RSA key used as HMAC secret (algorithm confusion attack).`
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return { detected: false, attack: null, alg, detail: "" };
|
|
855
|
+
}
|
|
856
|
+
function scanRequestForJwtAttacks(headers, body, query) {
|
|
857
|
+
const candidates = [];
|
|
858
|
+
const auth = headers["authorization"];
|
|
859
|
+
const authStr = Array.isArray(auth) ? auth[0] : auth;
|
|
860
|
+
if (typeof authStr === "string" && authStr.toLowerCase().startsWith("bearer ")) {
|
|
861
|
+
candidates.push(authStr.slice(7).trim());
|
|
862
|
+
}
|
|
863
|
+
const TOKEN_FIELDS = /* @__PURE__ */ new Set(["token", "jwt", "access_token", "accessToken", "id_token", "idToken", "refresh_token", "refreshToken"]);
|
|
864
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
865
|
+
for (const [key, val] of Object.entries(body)) {
|
|
866
|
+
if (TOKEN_FIELDS.has(key) && typeof val === "string" && val.includes(".")) {
|
|
867
|
+
candidates.push(val);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
for (const field of ["token", "jwt", "access_token"]) {
|
|
872
|
+
const val = query[field];
|
|
873
|
+
const str2 = Array.isArray(val) ? val[0] : val;
|
|
874
|
+
if (typeof str2 === "string" && str2.includes(".")) candidates.push(str2);
|
|
875
|
+
}
|
|
876
|
+
for (const token of candidates) {
|
|
877
|
+
const result = analyseJwt(token);
|
|
878
|
+
if (result.detected) return result;
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
625
882
|
function detectHoneypotType(path) {
|
|
626
883
|
const p = path.toLowerCase().split("?")[0] ?? path.toLowerCase();
|
|
627
884
|
if (/\/\.env(\.[\w]+)?$/.test(p) || p.endsWith("/.env")) return "env_file";
|
|
@@ -1314,7 +1571,7 @@ var AnomiraClient = class {
|
|
|
1314
1571
|
service: "app",
|
|
1315
1572
|
getUserId: defaultGetUserId,
|
|
1316
1573
|
getIp: defaultGetIp,
|
|
1317
|
-
detect: { bruteForce: true, rateAbuse: true, pathTraversal: true, xss: true, scanDetection: true, geoVelocity: true },
|
|
1574
|
+
detect: { bruteForce: true, rateAbuse: true, pathTraversal: true, xss: true, scanDetection: true, geoVelocity: true, ssrf: true, jwtManipulation: true },
|
|
1318
1575
|
autoBlock: { enabled: true, communityThreshold: 85 }
|
|
1319
1576
|
};
|
|
1320
1577
|
this.buffer = new EventBuffer({ appId: "", apiKey: "", ingestUrl: DEFAULT_INGEST_URL, maxBatchSize: 0, flushIntervalMs: 999999999, maxRetries: 0, debug: false });
|
|
@@ -1339,7 +1596,9 @@ var AnomiraClient = class {
|
|
|
1339
1596
|
pathTraversal: config.detect?.pathTraversal ?? true,
|
|
1340
1597
|
xss: config.detect?.xss ?? true,
|
|
1341
1598
|
scanDetection: config.detect?.scanDetection ?? true,
|
|
1342
|
-
geoVelocity: config.detect?.geoVelocity ?? true
|
|
1599
|
+
geoVelocity: config.detect?.geoVelocity ?? true,
|
|
1600
|
+
ssrf: config.detect?.ssrf ?? true,
|
|
1601
|
+
jwtManipulation: config.detect?.jwtManipulation ?? true
|
|
1343
1602
|
},
|
|
1344
1603
|
autoBlock: {
|
|
1345
1604
|
enabled: config.autoBlock?.enabled ?? true,
|
|
@@ -1696,7 +1955,7 @@ var AnomiraClient = class {
|
|
|
1696
1955
|
if (this.disabled) return;
|
|
1697
1956
|
const ctx = requestContext.getStore();
|
|
1698
1957
|
const resolvedIp = data.ip && data.ip !== "0.0.0.0" && data.ip !== "" ? data.ip : ctx?.ip ?? "0.0.0.0";
|
|
1699
|
-
if (this.config.debug && data.ip &&
|
|
1958
|
+
if (this.config.debug && data.ip && isPrivateIp3(data.ip)) {
|
|
1700
1959
|
this._origWarn(
|
|
1701
1960
|
`[Anomira] Warning: track() received a private/loopback IP "${data.ip}" for event "${eventName}". Behind a reverse proxy, req.ip is the proxy's address \u2014 use sentinel.getClientIp(req) instead.`
|
|
1702
1961
|
);
|
|
@@ -1935,13 +2194,13 @@ function pickId(obj) {
|
|
|
1935
2194
|
obj["accountId"] ?? obj["account_id"] ?? obj["customerId"] ?? obj["customer_id"];
|
|
1936
2195
|
return typeof id === "string" && id.length > 0 ? id : void 0;
|
|
1937
2196
|
}
|
|
1938
|
-
function
|
|
2197
|
+
function normalizeIp2(raw) {
|
|
1939
2198
|
if (raw === "::1") return "127.0.0.1";
|
|
1940
2199
|
if (raw === "::ffff:127.0.0.1") return "127.0.0.1";
|
|
1941
2200
|
if (raw.startsWith("::ffff:")) return raw.slice(7);
|
|
1942
2201
|
return raw;
|
|
1943
2202
|
}
|
|
1944
|
-
function
|
|
2203
|
+
function isPrivateIp3(ip) {
|
|
1945
2204
|
return ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0" || ip.startsWith("10.") || ip.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[01])\./.test(ip);
|
|
1946
2205
|
}
|
|
1947
2206
|
function defaultGetIp(req) {
|
|
@@ -1961,7 +2220,7 @@ function defaultGetIp(req) {
|
|
|
1961
2220
|
firstHdr("x-client-ip") ?? // Generic reverse proxies
|
|
1962
2221
|
firstHdr("x-cluster-client-ip") ?? // Cluster / k8s ingress
|
|
1963
2222
|
r["socket"]?.remoteAddress ?? "0.0.0.0";
|
|
1964
|
-
return
|
|
2223
|
+
return normalizeIp2(ip);
|
|
1965
2224
|
}
|
|
1966
2225
|
function sendFakeResponse(res, fakeResponse) {
|
|
1967
2226
|
const allHeaders = {
|
|
@@ -2204,6 +2463,31 @@ function createExpressMiddleware(client) {
|
|
|
2204
2463
|
client.track(EventName.XSS_DETECTED, { ip, userId, meta: { url, method } });
|
|
2205
2464
|
}
|
|
2206
2465
|
}
|
|
2466
|
+
if (client.config.detect.ssrf) {
|
|
2467
|
+
const query = req["query"] ?? {};
|
|
2468
|
+
const body = req["body"] ?? {};
|
|
2469
|
+
const signal = scanForSsrf(body, query);
|
|
2470
|
+
if (signal) {
|
|
2471
|
+
client.track(EventName.SSRF_ATTEMPT, {
|
|
2472
|
+
ip,
|
|
2473
|
+
userId,
|
|
2474
|
+
meta: { url, method, ssrfPayload: signal.payload, ssrfField: signal.field, ssrfReason: signal.reason }
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (client.config.detect.jwtManipulation) {
|
|
2479
|
+
const reqHeaders = headers ?? {};
|
|
2480
|
+
const reqBody = req["body"] ?? {};
|
|
2481
|
+
const reqQuery = req["query"] ?? {};
|
|
2482
|
+
const jwtResult = scanRequestForJwtAttacks(reqHeaders, reqBody, reqQuery);
|
|
2483
|
+
if (jwtResult?.detected) {
|
|
2484
|
+
client.track(EventName.JWT_MANIPULATION, {
|
|
2485
|
+
ip,
|
|
2486
|
+
userId,
|
|
2487
|
+
meta: { url, method, jwtAttack: jwtResult.attack, jwtAlg: jwtResult.alg, jwtDetail: jwtResult.detail }
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2207
2491
|
const onFinish = () => {
|
|
2208
2492
|
const lateUserId = client.config.getUserId(req) || browserFp?.uid || "";
|
|
2209
2493
|
if (!userIdWarnFired && userIdCheckCount < 20) {
|
|
@@ -2430,6 +2714,31 @@ function createFastifyPlugin(client) {
|
|
|
2430
2714
|
client.track(EventName.XSS_DETECTED, { ip, userId, meta: { url, method } });
|
|
2431
2715
|
}
|
|
2432
2716
|
}
|
|
2717
|
+
if (client.config.detect.ssrf) {
|
|
2718
|
+
const query = req["query"] ?? {};
|
|
2719
|
+
const body = req["body"] ?? {};
|
|
2720
|
+
const signal = scanForSsrf(body, query);
|
|
2721
|
+
if (signal) {
|
|
2722
|
+
client.track(EventName.SSRF_ATTEMPT, {
|
|
2723
|
+
ip,
|
|
2724
|
+
userId,
|
|
2725
|
+
meta: { url, method, ssrfPayload: signal.payload, ssrfField: signal.field, ssrfReason: signal.reason }
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
if (client.config.detect.jwtManipulation) {
|
|
2730
|
+
const fHeaders = req["headers"] ?? {};
|
|
2731
|
+
const fBody = req["body"] ?? {};
|
|
2732
|
+
const fQuery = req["query"] ?? {};
|
|
2733
|
+
const jwtRes = scanRequestForJwtAttacks(fHeaders, fBody, fQuery);
|
|
2734
|
+
if (jwtRes?.detected) {
|
|
2735
|
+
client.track(EventName.JWT_MANIPULATION, {
|
|
2736
|
+
ip,
|
|
2737
|
+
userId,
|
|
2738
|
+
meta: { url, method, jwtAttack: jwtRes.attack, jwtAlg: jwtRes.alg, jwtDetail: jwtRes.detail }
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2433
2742
|
});
|
|
2434
2743
|
fastify.addHook("onResponse", (req, reply) => {
|
|
2435
2744
|
const ip = client.config.getIp(req);
|
|
@@ -2585,23 +2894,40 @@ function createExpressMiddleware2(client) {
|
|
|
2585
2894
|
} catch {
|
|
2586
2895
|
}
|
|
2587
2896
|
}
|
|
2897
|
+
const startTs = process.hrtime.bigint();
|
|
2898
|
+
let resSize = 0;
|
|
2899
|
+
let resFieldCount = 0;
|
|
2900
|
+
const origJson = res.json.bind(res);
|
|
2901
|
+
res.json = function anomiraJson(body) {
|
|
2902
|
+
try {
|
|
2903
|
+
if (body !== null && typeof body === "object" && !Array.isArray(body)) {
|
|
2904
|
+
resFieldCount = Object.keys(body).length;
|
|
2905
|
+
}
|
|
2906
|
+
const serialised = JSON.stringify(body);
|
|
2907
|
+
resSize = Buffer.byteLength(serialised, "utf8");
|
|
2908
|
+
} catch {
|
|
2909
|
+
}
|
|
2910
|
+
return origJson(body);
|
|
2911
|
+
};
|
|
2912
|
+
const origSend = res.send.bind(res);
|
|
2913
|
+
res.send = function anomiraSend(body) {
|
|
2914
|
+
if (resSize === 0) {
|
|
2915
|
+
if (typeof body === "string") resSize = Buffer.byteLength(body, "utf8");
|
|
2916
|
+
else if (Buffer.isBuffer(body)) resSize = body.length;
|
|
2917
|
+
}
|
|
2918
|
+
return origSend(body);
|
|
2919
|
+
};
|
|
2588
2920
|
const onFinish = () => {
|
|
2589
2921
|
res.off("finish", onFinish);
|
|
2590
2922
|
const { statusCode } = res;
|
|
2923
|
+
const resTimeMs = Math.round(Number(process.hrtime.bigint() - startTs) / 1e6);
|
|
2924
|
+
const resMeta = { url, method, statusCode, resTimeMs, resSize, resFieldCount };
|
|
2591
2925
|
if (client.config.detect.rateAbuse && statusCode === 429) {
|
|
2592
|
-
client.track(EventName.RATE_LIMIT, {
|
|
2593
|
-
ip,
|
|
2594
|
-
userId,
|
|
2595
|
-
meta: { url, method, statusCode }
|
|
2596
|
-
});
|
|
2926
|
+
client.track(EventName.RATE_LIMIT, { ip, userId, meta: resMeta });
|
|
2597
2927
|
}
|
|
2598
2928
|
if (client.config.detect.bruteForce && statusCode === 401) {
|
|
2599
2929
|
if (/\/(login|signin|auth|token|session)/i.test(url)) {
|
|
2600
|
-
client.track(EventName.LOGIN_FAILED, {
|
|
2601
|
-
ip,
|
|
2602
|
-
userId,
|
|
2603
|
-
meta: { url, method, statusCode }
|
|
2604
|
-
});
|
|
2930
|
+
client.track(EventName.LOGIN_FAILED, { ip, userId, meta: resMeta });
|
|
2605
2931
|
}
|
|
2606
2932
|
}
|
|
2607
2933
|
if (client.config.detect.scanDetection && statusCode === 404) {
|
|
@@ -2610,14 +2936,14 @@ function createExpressMiddleware2(client) {
|
|
|
2610
2936
|
client.track(EventName.SCAN_DETECTED, {
|
|
2611
2937
|
ip,
|
|
2612
2938
|
userId,
|
|
2613
|
-
meta: {
|
|
2939
|
+
meta: { ...resMeta, userAgent: ua }
|
|
2614
2940
|
});
|
|
2615
2941
|
}
|
|
2616
2942
|
}
|
|
2617
2943
|
if (client.config.detect.geoVelocity && statusCode === 200 && /\/(login|signin|auth|token)/i.test(url) && method === "POST") {
|
|
2618
2944
|
const resolvedUserId = client.config.getUserId(req);
|
|
2619
2945
|
if (resolvedUserId) {
|
|
2620
|
-
void client.trackLogin({ ip, userId: resolvedUserId, meta:
|
|
2946
|
+
void client.trackLogin({ ip, userId: resolvedUserId, meta: resMeta });
|
|
2621
2947
|
}
|
|
2622
2948
|
}
|
|
2623
2949
|
};
|
|
@@ -2657,30 +2983,52 @@ function createFastifyPlugin2(client) {
|
|
|
2657
2983
|
}
|
|
2658
2984
|
}
|
|
2659
2985
|
});
|
|
2986
|
+
fastify.addHook("onSend", async (_req, _reply, payload) => {
|
|
2987
|
+
try {
|
|
2988
|
+
if (typeof payload === "string") {
|
|
2989
|
+
_req._anomiraResSize = Buffer.byteLength(payload, "utf8");
|
|
2990
|
+
try {
|
|
2991
|
+
const parsed = JSON.parse(payload);
|
|
2992
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2993
|
+
_req._anomiraResFields = Object.keys(parsed).length;
|
|
2994
|
+
}
|
|
2995
|
+
} catch {
|
|
2996
|
+
}
|
|
2997
|
+
} else if (Buffer.isBuffer(payload)) {
|
|
2998
|
+
_req._anomiraResSize = payload.length;
|
|
2999
|
+
}
|
|
3000
|
+
} catch {
|
|
3001
|
+
}
|
|
3002
|
+
return payload;
|
|
3003
|
+
});
|
|
2660
3004
|
fastify.addHook("onResponse", async (req, reply) => {
|
|
2661
3005
|
const ip = client.config.getIp(req);
|
|
2662
3006
|
const userId = client.config.getUserId(req);
|
|
2663
3007
|
const url = req.url;
|
|
2664
3008
|
const method = req.method.toUpperCase();
|
|
2665
3009
|
const statusCode = reply.statusCode;
|
|
3010
|
+
const resTimeMs = Math.round(reply.elapsedTime ?? 0);
|
|
3011
|
+
const resSize = req._anomiraResSize ?? 0;
|
|
3012
|
+
const resFieldCount = req._anomiraResFields ?? 0;
|
|
3013
|
+
const resMeta = { url, method, statusCode, resTimeMs, resSize, resFieldCount };
|
|
2666
3014
|
if (client.config.detect.rateAbuse && statusCode === 429) {
|
|
2667
|
-
client.track(EventName.RATE_LIMIT, { ip, userId, meta:
|
|
3015
|
+
client.track(EventName.RATE_LIMIT, { ip, userId, meta: resMeta });
|
|
2668
3016
|
}
|
|
2669
3017
|
if (client.config.detect.bruteForce && statusCode === 401) {
|
|
2670
3018
|
if (/\/(login|signin|auth|token|session)/i.test(url)) {
|
|
2671
|
-
client.track(EventName.LOGIN_FAILED, { ip, userId, meta:
|
|
3019
|
+
client.track(EventName.LOGIN_FAILED, { ip, userId, meta: resMeta });
|
|
2672
3020
|
}
|
|
2673
3021
|
}
|
|
2674
3022
|
if (client.config.detect.scanDetection && statusCode === 404) {
|
|
2675
3023
|
const ua = req.headers["user-agent"] ?? "";
|
|
2676
3024
|
if (!ua || /curl|wget|python|go-http|nuclei|sqlmap|nikto/i.test(ua)) {
|
|
2677
|
-
client.track(EventName.SCAN_DETECTED, { ip, userId, meta: {
|
|
3025
|
+
client.track(EventName.SCAN_DETECTED, { ip, userId, meta: { ...resMeta, userAgent: ua } });
|
|
2678
3026
|
}
|
|
2679
3027
|
}
|
|
2680
3028
|
if (client.config.detect.geoVelocity && statusCode === 200 && method === "POST" && /\/(login|signin|auth|token)/i.test(url)) {
|
|
2681
3029
|
const resolvedUserId = client.config.getUserId(req);
|
|
2682
3030
|
if (resolvedUserId) {
|
|
2683
|
-
void client.trackLogin({ ip, userId: resolvedUserId, meta:
|
|
3031
|
+
void client.trackLogin({ ip, userId: resolvedUserId, meta: resMeta });
|
|
2684
3032
|
}
|
|
2685
3033
|
}
|
|
2686
3034
|
});
|