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