fullshield 0.0.0-stage → 0.1.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/CHANGELOG.md +77 -0
- package/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/agent.js +5737 -0
- package/dist/cli/index.js +8540 -0
- package/dist/compliance.js +126 -0
- package/dist/confirm.js +146 -0
- package/dist/dashboard.js +121 -0
- package/dist/iac.js +84 -0
- package/dist/incremental.js +13 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +6180 -0
- package/dist/llm.js +105 -0
- package/dist/mcp.js +5831 -0
- package/dist/runtime/auto.js +1163 -0
- package/dist/runtime/express.js +229 -0
- package/dist/runtime/fastify.js +241 -0
- package/dist/runtime/index.js +216 -0
- package/dist/runtime/rasp/loader.js +14 -0
- package/dist/runtime/rasp.js +1339 -0
- package/dist/runtime/register.js +1171 -0
- package/dist/supply.js +228 -0
- package/dist/threat.js +63 -0
- package/dist/workspaces.js +23 -0
- package/package.json +88 -4
- package/rules/CONFIG-BODYLIMIT-001.md +9 -0
- package/rules/CONFIG-CORS-001.md +9 -0
- package/rules/CONFIG-DOCKER-ENV-001.md +9 -0
- package/rules/CONFIG-DOCKER-LATEST-001.md +9 -0
- package/rules/CONFIG-DOCKER-ROOT-001.md +9 -0
- package/rules/CONFIG-ERRSTACK-001.md +9 -0
- package/rules/CONFIG-HELMET-001.md +9 -0
- package/rules/CONFIG-INSTALLSCRIPT-001.md +9 -0
- package/rules/CONFIG-RATELIMIT-001.md +9 -0
- package/rules/DEPS-DEPRECATED-001.md +9 -0
- package/rules/DEPS-NOLOCK-001.md +9 -0
- package/rules/DEPS-TYPOSQUAT-001.md +9 -0
- package/rules/DEPS-VULN-001.md +9 -0
- package/rules/README.md +60 -0
- package/rules/SAST-AUTH-001.md +9 -0
- package/rules/SAST-CMDI-001.md +9 -0
- package/rules/SAST-CRYPTO-001.md +9 -0
- package/rules/SAST-EVAL-001.md +9 -0
- package/rules/SAST-JWT-001.md +9 -0
- package/rules/SAST-NOSQLI-001.md +9 -0
- package/rules/SAST-PATH-001.md +9 -0
- package/rules/SAST-PROTO-001.md +9 -0
- package/rules/SAST-REDIRECT-001.md +9 -0
- package/rules/SAST-SQLI-001.md +9 -0
- package/rules/SAST-SSRF-001.md +9 -0
- package/rules/SAST-XSS-001.md +9 -0
- package/rules/SECRET-AIKEY-001.md +9 -0
- package/rules/SECRET-AWS-001.md +9 -0
- package/rules/SECRET-DBURL-001.md +9 -0
- package/rules/SECRET-ENTROPY-001.md +9 -0
- package/rules/SECRET-ENVFILE-001.md +9 -0
- package/rules/SECRET-GITHUB-001.md +9 -0
- package/rules/SECRET-GOOGLE-001.md +9 -0
- package/rules/SECRET-JWT-001.md +9 -0
- package/rules/SECRET-PRIVKEY-001.md +9 -0
- package/rules/SECRET-SLACK-001.md +9 -0
- package/rules/SECRET-STRIPE-001.md +9 -0
- package/templates/fullshield.config.json +7 -0
- package/templates/github-watch.yml +15 -0
- package/templates/github-workflow.yml +19 -0
- package/templates/gitlab-ci.yml +11 -0
- package/templates/pre-commit.sh +11 -0
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
import dns from 'dns';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
4
|
+
|
|
5
|
+
// src/runtime/rasp/nosql.ts
|
|
6
|
+
function isOperatorObject(value) {
|
|
7
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
8
|
+
return Object.keys(value).some((k) => k.startsWith("$"));
|
|
9
|
+
}
|
|
10
|
+
function deepEqual(a, b) {
|
|
11
|
+
if (a === b) return true;
|
|
12
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
13
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
14
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
15
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
16
|
+
}
|
|
17
|
+
const ak = Object.keys(a);
|
|
18
|
+
const bk = Object.keys(b);
|
|
19
|
+
if (ak.length !== bk.length) return false;
|
|
20
|
+
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
21
|
+
}
|
|
22
|
+
function collect(value, out, depth) {
|
|
23
|
+
if (depth > 8 || value === null || typeof value !== "object") return;
|
|
24
|
+
out.push(value);
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
for (const item of value) collect(item, out, depth + 1);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (const child of Object.values(value)) collect(child, out, depth + 1);
|
|
30
|
+
}
|
|
31
|
+
function nosqlUserOperators(sink, ctx) {
|
|
32
|
+
const userObjects = [];
|
|
33
|
+
for (const obj of ctx.objects) collect(obj, userObjects, 0);
|
|
34
|
+
const userOps = userObjects.filter(isOperatorObject);
|
|
35
|
+
if (userOps.length === 0) return false;
|
|
36
|
+
const seen = /* @__PURE__ */ new Set();
|
|
37
|
+
const walk2 = (value, depth) => {
|
|
38
|
+
if (depth > 8 || value === null || typeof value !== "object") return false;
|
|
39
|
+
if (seen.has(value)) return false;
|
|
40
|
+
seen.add(value);
|
|
41
|
+
if (isOperatorObject(value) && userOps.some((u) => u === value || deepEqual(u, value))) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(value)) return value.some((item) => walk2(item, depth + 1));
|
|
45
|
+
return Object.values(value).some((child) => walk2(child, depth + 1));
|
|
46
|
+
};
|
|
47
|
+
return walk2(sink, 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/runtime/rasp/path.ts
|
|
51
|
+
function logicalResolve(p) {
|
|
52
|
+
const isAbs = p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
|
53
|
+
const parts = [];
|
|
54
|
+
for (const seg of p.split(/[\\/]+/)) {
|
|
55
|
+
if (seg === "" || seg === ".") continue;
|
|
56
|
+
if (seg === "..") {
|
|
57
|
+
parts.pop();
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
parts.push(seg);
|
|
61
|
+
}
|
|
62
|
+
return (isAbs ? "/" : "") + parts.join("/");
|
|
63
|
+
}
|
|
64
|
+
function isInside(base, target) {
|
|
65
|
+
const b = logicalResolve(base).replace(/\/+$/, "");
|
|
66
|
+
const t = logicalResolve(target);
|
|
67
|
+
if (b === "") return !t.startsWith("/") && !t.split("/").includes("..");
|
|
68
|
+
return t === b || t.startsWith(`${b}/`);
|
|
69
|
+
}
|
|
70
|
+
function inferBase(sink, input) {
|
|
71
|
+
const idx = sink.indexOf(input);
|
|
72
|
+
if (idx <= 0) return "";
|
|
73
|
+
return sink.slice(0, idx);
|
|
74
|
+
}
|
|
75
|
+
function lastSeg(p) {
|
|
76
|
+
const parts = logicalResolve(p).split("/").filter(Boolean);
|
|
77
|
+
return parts[parts.length - 1];
|
|
78
|
+
}
|
|
79
|
+
function pathInputEscapes(sink, input, base) {
|
|
80
|
+
if (!input) return false;
|
|
81
|
+
if (input.includes("\0") || sink.includes("\0")) return true;
|
|
82
|
+
if (sink.includes(input)) {
|
|
83
|
+
if (input.includes("..")) return true;
|
|
84
|
+
const intended = base !== void 0 && base.length > 0 ? base : inferBase(sink, input);
|
|
85
|
+
if (intended === "") {
|
|
86
|
+
return input.startsWith("/") || /^[A-Za-z]:[\\/]/.test(input) || input.includes("..");
|
|
87
|
+
}
|
|
88
|
+
return !isInside(intended, sink);
|
|
89
|
+
}
|
|
90
|
+
if (input.includes("..") || input.startsWith("/") || /^[A-Za-z]:[\\/]/.test(input)) {
|
|
91
|
+
const a = lastSeg(input);
|
|
92
|
+
const b = lastSeg(sink);
|
|
93
|
+
if (a && a === b) return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/runtime/rasp/proto.ts
|
|
99
|
+
var DANGEROUS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
100
|
+
function walk(value, depth, seen) {
|
|
101
|
+
if (depth > 8 || value === null || typeof value !== "object") return false;
|
|
102
|
+
if (seen.has(value)) return false;
|
|
103
|
+
seen.add(value);
|
|
104
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
105
|
+
if (DANGEROUS.has(key)) return true;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(value)) return value.some((item) => walk(item, depth + 1, seen));
|
|
108
|
+
return Object.values(value).some((child) => walk(child, depth + 1, seen));
|
|
109
|
+
}
|
|
110
|
+
function protoPolluted(ctx) {
|
|
111
|
+
const seen = /* @__PURE__ */ new Set();
|
|
112
|
+
return ctx.objects.some((obj) => walk(obj, 0, seen));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/runtime/rasp/shell.ts
|
|
116
|
+
function tokenizeShell(command) {
|
|
117
|
+
const tokens = [];
|
|
118
|
+
let i = 0;
|
|
119
|
+
const n = command.length;
|
|
120
|
+
while (i < n) {
|
|
121
|
+
const c = command[i];
|
|
122
|
+
if (c === " " || c === " " || c === "\n") {
|
|
123
|
+
const start2 = i;
|
|
124
|
+
while (i < n && (command[i] === " " || command[i] === " " || command[i] === "\n")) i += 1;
|
|
125
|
+
tokens.push({ kind: "space", start: start2, end: i });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (c === "'" || c === '"') {
|
|
129
|
+
const quote = c;
|
|
130
|
+
const start2 = i;
|
|
131
|
+
i += 1;
|
|
132
|
+
while (i < n && command[i] !== quote) {
|
|
133
|
+
if (quote === '"' && command[i] === "\\") i += 1;
|
|
134
|
+
i += 1;
|
|
135
|
+
}
|
|
136
|
+
if (i < n) i += 1;
|
|
137
|
+
tokens.push({ kind: "word", start: start2, end: i });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (c === "`") {
|
|
141
|
+
const start2 = i;
|
|
142
|
+
i += 1;
|
|
143
|
+
while (i < n && command[i] !== "`") i += 1;
|
|
144
|
+
if (i < n) i += 1;
|
|
145
|
+
tokens.push({ kind: "op", start: start2, end: i });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (c === "$" && (command[i + 1] === "(" || command[i + 1] === "{")) {
|
|
149
|
+
const open = command[i + 1];
|
|
150
|
+
const close = open === "(" ? ")" : "}";
|
|
151
|
+
const start2 = i;
|
|
152
|
+
i += 2;
|
|
153
|
+
let depth = 1;
|
|
154
|
+
while (i < n && depth > 0) {
|
|
155
|
+
if (command[i] === open) depth += 1;
|
|
156
|
+
else if (command[i] === close) depth -= 1;
|
|
157
|
+
i += 1;
|
|
158
|
+
}
|
|
159
|
+
tokens.push({ kind: "op", start: start2, end: i });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (command.startsWith("||", i) || command.startsWith("&&", i) || command.startsWith(">>", i) || command.startsWith("<<", i)) {
|
|
163
|
+
tokens.push({ kind: "op", start: i, end: i + 2 });
|
|
164
|
+
i += 2;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (";|&<>()".includes(c)) {
|
|
168
|
+
tokens.push({ kind: "op", start: i, end: i + 1 });
|
|
169
|
+
i += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const start = i;
|
|
173
|
+
while (i < n && !/[\s;|&$`<>()]/.test(command[i])) i += 1;
|
|
174
|
+
tokens.push({ kind: "word", start, end: i });
|
|
175
|
+
}
|
|
176
|
+
return tokens;
|
|
177
|
+
}
|
|
178
|
+
function shellInputChangesStructure(command, input) {
|
|
179
|
+
if (!input) return false;
|
|
180
|
+
const tokens = tokenizeShell(command);
|
|
181
|
+
let from = 0;
|
|
182
|
+
while (from <= command.length) {
|
|
183
|
+
const idx = command.indexOf(input, from);
|
|
184
|
+
if (idx === -1) return false;
|
|
185
|
+
const end = idx + input.length;
|
|
186
|
+
const hit = tokens.filter((t) => t.kind !== "space" && t.start < end && t.end > idx);
|
|
187
|
+
if (!(hit.length === 1 && hit[0].kind === "word" && hit[0].start <= idx && hit[0].end >= end)) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
from = idx + Math.max(input.length, 1);
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/runtime/rasp/sql.ts
|
|
196
|
+
function tokenizeSql(sql) {
|
|
197
|
+
const tokens = [];
|
|
198
|
+
let i = 0;
|
|
199
|
+
const n = sql.length;
|
|
200
|
+
while (i < n) {
|
|
201
|
+
const c = sql[i];
|
|
202
|
+
if (c <= " ") {
|
|
203
|
+
const start2 = i;
|
|
204
|
+
while (i < n && sql[i] <= " ") i += 1;
|
|
205
|
+
tokens.push({ kind: "space", start: start2, end: i });
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (c === "'" || c === '"' || c === "`") {
|
|
209
|
+
const quote = c;
|
|
210
|
+
const start2 = i;
|
|
211
|
+
i += 1;
|
|
212
|
+
while (i < n) {
|
|
213
|
+
if (sql[i] === quote) {
|
|
214
|
+
if (sql[i + 1] === quote) {
|
|
215
|
+
i += 2;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
i += 1;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
i += 1;
|
|
222
|
+
}
|
|
223
|
+
tokens.push({ kind: "string", start: start2, end: i });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (c === "-" && sql[i + 1] === "-") {
|
|
227
|
+
const start2 = i;
|
|
228
|
+
i += 2;
|
|
229
|
+
while (i < n && sql[i] !== "\n") i += 1;
|
|
230
|
+
tokens.push({ kind: "comment", start: start2, end: i });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (c === "/" && sql[i + 1] === "*") {
|
|
234
|
+
const start2 = i;
|
|
235
|
+
i += 2;
|
|
236
|
+
while (i < n && !(sql[i] === "*" && sql[i + 1] === "/")) i += 1;
|
|
237
|
+
if (i + 1 < n) i += 2;
|
|
238
|
+
tokens.push({ kind: "comment", start: start2, end: i });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (c === "#") {
|
|
242
|
+
const start2 = i;
|
|
243
|
+
while (i < n && sql[i] !== "\n") i += 1;
|
|
244
|
+
tokens.push({ kind: "comment", start: start2, end: i });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (c >= "0" && c <= "9" || c === "." && i + 1 < n && sql[i + 1] >= "0" && sql[i + 1] <= "9") {
|
|
248
|
+
const start2 = i;
|
|
249
|
+
while (i < n && /[0-9.eE+-]/.test(sql[i])) i += 1;
|
|
250
|
+
tokens.push({ kind: "number", start: start2, end: i });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
254
|
+
const start2 = i;
|
|
255
|
+
while (i < n && /[A-Za-z0-9_$]/.test(sql[i])) i += 1;
|
|
256
|
+
tokens.push({ kind: "ident", start: start2, end: i });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const start = i;
|
|
260
|
+
if (sql.startsWith("<=", i) || sql.startsWith(">=", i) || sql.startsWith("<>", i) || sql.startsWith("!=", i) || sql.startsWith("||", i) || sql.startsWith("&&", i)) {
|
|
261
|
+
i += 2;
|
|
262
|
+
} else {
|
|
263
|
+
i += 1;
|
|
264
|
+
}
|
|
265
|
+
tokens.push({ kind: "op", start, end: i });
|
|
266
|
+
}
|
|
267
|
+
return tokens;
|
|
268
|
+
}
|
|
269
|
+
function spanUnsafe(tokens, start, end) {
|
|
270
|
+
const hit = tokens.filter((t) => t.kind !== "space" && t.start < end && t.end > start);
|
|
271
|
+
if (hit.length === 1) {
|
|
272
|
+
const t = hit[0];
|
|
273
|
+
const safe = t.kind === "string" || t.kind === "number" || t.kind === "comment";
|
|
274
|
+
return !safe || t.start > start || t.end < end;
|
|
275
|
+
}
|
|
276
|
+
return hit.length > 0;
|
|
277
|
+
}
|
|
278
|
+
function sqlInputChangesStructure(query, input) {
|
|
279
|
+
if (!input) return false;
|
|
280
|
+
const tokens = tokenizeSql(query);
|
|
281
|
+
let from = 0;
|
|
282
|
+
while (from <= query.length) {
|
|
283
|
+
const idx = query.indexOf(input, from);
|
|
284
|
+
if (idx === -1) return false;
|
|
285
|
+
if (spanUnsafe(tokens, idx, idx + input.length)) return true;
|
|
286
|
+
from = idx + Math.max(input.length, 1);
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
function normalizeHost(host) {
|
|
291
|
+
let h = host.trim().toLowerCase();
|
|
292
|
+
h = h.replace(/\.+$/, "");
|
|
293
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
294
|
+
if (h.startsWith("::ffff:")) h = h.slice(7);
|
|
295
|
+
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
296
|
+
return h;
|
|
297
|
+
}
|
|
298
|
+
function parseIpv4Part(part) {
|
|
299
|
+
if (/^0x[0-9a-f]+$/i.test(part)) {
|
|
300
|
+
const n = Number.parseInt(part, 16);
|
|
301
|
+
return Number.isFinite(n) ? n : void 0;
|
|
302
|
+
}
|
|
303
|
+
if (/^0[0-7]+$/.test(part)) {
|
|
304
|
+
const n = Number.parseInt(part, 8);
|
|
305
|
+
return Number.isFinite(n) ? n : void 0;
|
|
306
|
+
}
|
|
307
|
+
if (/^\d+$/.test(part)) {
|
|
308
|
+
const n = Number(part);
|
|
309
|
+
return Number.isFinite(n) ? n : void 0;
|
|
310
|
+
}
|
|
311
|
+
return void 0;
|
|
312
|
+
}
|
|
313
|
+
function parseIpv4(host) {
|
|
314
|
+
const h = normalizeHost(host);
|
|
315
|
+
if (!h || h.includes(":")) return void 0;
|
|
316
|
+
const parts = h.split(".");
|
|
317
|
+
if (parts.length === 0 || parts.length > 4) return void 0;
|
|
318
|
+
const nums = parts.map(parseIpv4Part);
|
|
319
|
+
if (nums.some((n) => n === void 0)) return void 0;
|
|
320
|
+
const a = nums[0];
|
|
321
|
+
if (parts.length === 1) {
|
|
322
|
+
if (a > 4294967295) return void 0;
|
|
323
|
+
return [a >>> 24 & 255, a >>> 16 & 255, a >>> 8 & 255, a & 255];
|
|
324
|
+
}
|
|
325
|
+
if (parts.length === 2) {
|
|
326
|
+
const b2 = nums[1];
|
|
327
|
+
if (a > 255 || b2 > 16777215) return void 0;
|
|
328
|
+
return [a, b2 >>> 16 & 255, b2 >>> 8 & 255, b2 & 255];
|
|
329
|
+
}
|
|
330
|
+
if (parts.length === 3) {
|
|
331
|
+
const b2 = nums[1];
|
|
332
|
+
const c2 = nums[2];
|
|
333
|
+
if (a > 255 || b2 > 255 || c2 > 65535) return void 0;
|
|
334
|
+
return [a, b2, c2 >>> 8 & 255, c2 & 255];
|
|
335
|
+
}
|
|
336
|
+
const b = nums[1];
|
|
337
|
+
const c = nums[2];
|
|
338
|
+
const d = nums[3];
|
|
339
|
+
if ([a, b, c, d].some((n) => n > 255)) return void 0;
|
|
340
|
+
return [a, b, c, d];
|
|
341
|
+
}
|
|
342
|
+
function isPrivateV4(octets) {
|
|
343
|
+
const [a, b] = octets;
|
|
344
|
+
if (a === 127 || a === 0 || a === 10) return true;
|
|
345
|
+
if (a === 169 && b === 254) return true;
|
|
346
|
+
if (a === 192 && b === 168) return true;
|
|
347
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
function isPrivateIpv6(h) {
|
|
351
|
+
if (h === "::1" || h === "::" || h === "0:0:0:0:0:0:0:1") return true;
|
|
352
|
+
if (h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:")) return true;
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
function isPrivateOrMetadata(host) {
|
|
356
|
+
const h = normalizeHost(host);
|
|
357
|
+
if (h === "localhost" || h.endsWith(".localhost") || h === "metadata.google.internal") return true;
|
|
358
|
+
const v4 = parseIpv4(h);
|
|
359
|
+
if (v4) return isPrivateV4(v4);
|
|
360
|
+
if (h.includes(":")) return isPrivateIpv6(h);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
function defaultLookup(host) {
|
|
364
|
+
const h = normalizeHost(host);
|
|
365
|
+
if (parseIpv4(h) || h.includes(":")) return [h];
|
|
366
|
+
try {
|
|
367
|
+
const lookupSync = dns.lookupSync;
|
|
368
|
+
if (typeof lookupSync === "function") {
|
|
369
|
+
return lookupSync(h, { all: true }).map((row) => row.address);
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
function resolveHostAddresses(host) {
|
|
376
|
+
try {
|
|
377
|
+
return defaultLookup(host);
|
|
378
|
+
} catch {
|
|
379
|
+
return [];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function hostIsBlocked(host) {
|
|
383
|
+
if (isPrivateOrMetadata(host)) return true;
|
|
384
|
+
for (const addr of resolveHostAddresses(host)) {
|
|
385
|
+
if (isPrivateOrMetadata(addr)) return true;
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
function ssrfHostFromUser(url, input) {
|
|
390
|
+
if (!input || !url.includes(input)) return false;
|
|
391
|
+
let parsed;
|
|
392
|
+
try {
|
|
393
|
+
parsed = new URL(url);
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
const host = parsed.hostname;
|
|
398
|
+
if (!host) return false;
|
|
399
|
+
const rawAuth = /^https?:\/\/([^/?#]+)/i.exec(url)?.[1] ?? host;
|
|
400
|
+
const rawHost = rawAuth.replace(/:\d+$/, "");
|
|
401
|
+
const inputTouchesHost = input === url || input.includes(host) || input.includes(rawHost) || rawHost.includes(input) && input.length >= 3;
|
|
402
|
+
if (!inputTouchesHost) return false;
|
|
403
|
+
return hostIsBlocked(host) || hostIsBlocked(rawHost);
|
|
404
|
+
}
|
|
405
|
+
function urlFromHttpArgs(args) {
|
|
406
|
+
const first = args[0];
|
|
407
|
+
if (typeof first === "string") return first;
|
|
408
|
+
if (first instanceof URL) return first.href;
|
|
409
|
+
if (first && typeof first === "object") {
|
|
410
|
+
const o = first;
|
|
411
|
+
if (typeof o.href === "string") return o.href;
|
|
412
|
+
const proto = o.protocol ?? "http:";
|
|
413
|
+
const host = o.hostname ?? o.host ?? "";
|
|
414
|
+
const path = o.path ?? "/";
|
|
415
|
+
return `${proto}//${host}${path}`;
|
|
416
|
+
}
|
|
417
|
+
return "";
|
|
418
|
+
}
|
|
419
|
+
function fetchUrl(input) {
|
|
420
|
+
if (typeof input === "string") return input;
|
|
421
|
+
if (input instanceof URL) return input.href;
|
|
422
|
+
if (input && typeof input === "object" && "url" in input && typeof input.url === "string") {
|
|
423
|
+
return input.url;
|
|
424
|
+
}
|
|
425
|
+
return "";
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/runtime/rasp/types.ts
|
|
429
|
+
var RASP_RULE = {
|
|
430
|
+
sql: "RASP-SQL-001",
|
|
431
|
+
shell: "RASP-SHELL-001",
|
|
432
|
+
path: "RASP-PATH-001",
|
|
433
|
+
nosql: "RASP-NOSQL-001",
|
|
434
|
+
ssrf: "RASP-SSRF-001",
|
|
435
|
+
proto: "RASP-PROTO-001"
|
|
436
|
+
};
|
|
437
|
+
var RASP_FAIL_OPEN = "RASP-FAIL-OPEN-001";
|
|
438
|
+
var RaspBlockedError = class extends Error {
|
|
439
|
+
ruleId;
|
|
440
|
+
constructor(ruleId) {
|
|
441
|
+
super(`fullshield rasp blocked ${ruleId}`);
|
|
442
|
+
this.name = "RaspBlockedError";
|
|
443
|
+
this.ruleId = ruleId;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// src/runtime/rasp/decide.ts
|
|
448
|
+
function sinkTexts(sink) {
|
|
449
|
+
if (typeof sink === "string") return [sink];
|
|
450
|
+
if (typeof sink === "number" || typeof sink === "boolean") return [String(sink)];
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
function detectStructureChange(kind, sink, ctx, base) {
|
|
454
|
+
if (kind === "proto") return protoPolluted(ctx);
|
|
455
|
+
if (kind === "nosql") return nosqlUserOperators(sink, ctx);
|
|
456
|
+
for (const text of sinkTexts(sink)) {
|
|
457
|
+
for (const input of ctx.strings) {
|
|
458
|
+
if (!input) continue;
|
|
459
|
+
if (kind === "sql" && sqlInputChangesStructure(text, input)) return true;
|
|
460
|
+
if (kind === "shell" && shellInputChangesStructure(text, input)) return true;
|
|
461
|
+
if (kind === "path" && pathInputEscapes(text, input, base)) return true;
|
|
462
|
+
if (kind === "ssrf" && ssrfHostFromUser(text, input)) return true;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
function decideSink(kind, sink, ctx, base) {
|
|
468
|
+
const hit = detectStructureChange(kind, sink, ctx, base);
|
|
469
|
+
if (hit) return { block: true, ruleId: RASP_RULE[kind] };
|
|
470
|
+
return { block: false };
|
|
471
|
+
}
|
|
472
|
+
var SKIP_HEADERS = /* @__PURE__ */ new Set([
|
|
473
|
+
"host",
|
|
474
|
+
"connection",
|
|
475
|
+
"user-agent",
|
|
476
|
+
"accept",
|
|
477
|
+
"accept-encoding",
|
|
478
|
+
"accept-language",
|
|
479
|
+
"content-type",
|
|
480
|
+
"content-length",
|
|
481
|
+
"transfer-encoding",
|
|
482
|
+
"cookie"
|
|
483
|
+
]);
|
|
484
|
+
var storage = new AsyncLocalStorage();
|
|
485
|
+
function flatten(value, out, depth) {
|
|
486
|
+
if (depth > 8 || out.length > 200) return;
|
|
487
|
+
if (typeof value === "string") {
|
|
488
|
+
out.push(value);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
492
|
+
out.push(String(value));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (Array.isArray(value)) {
|
|
496
|
+
for (const item of value) flatten(item, out, depth + 1);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (value !== null && typeof value === "object") {
|
|
500
|
+
for (const child of Object.values(value)) flatten(child, out, depth + 1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function collectObjects(value, out, depth) {
|
|
504
|
+
if (depth > 8 || value === null || typeof value !== "object") return;
|
|
505
|
+
out.push(value);
|
|
506
|
+
if (Array.isArray(value)) {
|
|
507
|
+
for (const item of value) collectObjects(item, out, depth + 1);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
for (const child of Object.values(value)) collectObjects(child, out, depth + 1);
|
|
511
|
+
}
|
|
512
|
+
function contextFromRequest(req) {
|
|
513
|
+
const strings = [];
|
|
514
|
+
const objects = [];
|
|
515
|
+
for (const value of [req.query, req.body, req.params, req.cookies]) {
|
|
516
|
+
flatten(value, strings, 0);
|
|
517
|
+
collectObjects(value, objects, 0);
|
|
518
|
+
}
|
|
519
|
+
if (req.headers && typeof req.headers === "object") {
|
|
520
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
521
|
+
if (SKIP_HEADERS.has(key.toLowerCase())) continue;
|
|
522
|
+
flatten(value, strings, 0);
|
|
523
|
+
}
|
|
524
|
+
collectObjects(req.headers, objects, 0);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
path: req.path ?? req.url?.split("?")[0] ?? "/",
|
|
528
|
+
ip: req.ip ?? "unknown",
|
|
529
|
+
strings,
|
|
530
|
+
objects
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function enterRaspContext(ctx, fn) {
|
|
534
|
+
return storage.run(ctx, fn);
|
|
535
|
+
}
|
|
536
|
+
function currentRaspContext() {
|
|
537
|
+
return storage.getStore();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/runtime/rasp/egress.ts
|
|
541
|
+
function egressAllowed(host, allow) {
|
|
542
|
+
const h = host.toLowerCase().replace(/\.+$/, "");
|
|
543
|
+
for (const rule of allow) {
|
|
544
|
+
const r = rule.toLowerCase();
|
|
545
|
+
if (r.startsWith("*.")) {
|
|
546
|
+
const suffix = r.slice(1);
|
|
547
|
+
if (h.endsWith(suffix) && h !== r.slice(2)) return true;
|
|
548
|
+
} else if (h === r) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/runtime/rasp/threat.ts
|
|
556
|
+
var scores = /* @__PURE__ */ new Map();
|
|
557
|
+
var blockUntil = /* @__PURE__ */ new Map();
|
|
558
|
+
var WEIGHT = {
|
|
559
|
+
block: 2,
|
|
560
|
+
honey: 5,
|
|
561
|
+
anomaly: 1
|
|
562
|
+
};
|
|
563
|
+
var DEFAULT_MAX_BLOCK_MS = 15 * 60 * 1e3;
|
|
564
|
+
var autoBlock = false;
|
|
565
|
+
var maxBlockMs = DEFAULT_MAX_BLOCK_MS;
|
|
566
|
+
function configureThreat(opts = {}) {
|
|
567
|
+
autoBlock = opts.autoBlock === true;
|
|
568
|
+
if (typeof opts.maxBlockMs === "number" && opts.maxBlockMs > 0) maxBlockMs = opts.maxBlockMs;
|
|
569
|
+
}
|
|
570
|
+
function addThreat(ip, kind, now = Date.now()) {
|
|
571
|
+
const next = (scores.get(ip) ?? 0) + WEIGHT[kind];
|
|
572
|
+
scores.set(ip, next);
|
|
573
|
+
if (autoBlock && next >= 10) {
|
|
574
|
+
blockUntil.set(ip, now + maxBlockMs);
|
|
575
|
+
}
|
|
576
|
+
return next;
|
|
577
|
+
}
|
|
578
|
+
function isIpBlocked(ip, now = Date.now()) {
|
|
579
|
+
if (!autoBlock) return false;
|
|
580
|
+
const until = blockUntil.get(ip);
|
|
581
|
+
if (until === void 0) return false;
|
|
582
|
+
if (now >= until) {
|
|
583
|
+
blockUntil.delete(ip);
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/runtime/rasp/honey.ts
|
|
590
|
+
var DECOY_ROUTES = ["/admin.php", "/.git/config", "/wp-login.php"];
|
|
591
|
+
var registered = /* @__PURE__ */ new Set();
|
|
592
|
+
var ROUTE_WINDOW_MS = 6e4;
|
|
593
|
+
var routeHits = /* @__PURE__ */ new Map();
|
|
594
|
+
function isHoneyRoute(path) {
|
|
595
|
+
const p = path.split("?")[0] ?? path;
|
|
596
|
+
return DECOY_ROUTES.includes(p);
|
|
597
|
+
}
|
|
598
|
+
function isHoneyCanary(text) {
|
|
599
|
+
if (!text) return false;
|
|
600
|
+
for (const c of registered) {
|
|
601
|
+
if (c && text.includes(c)) return true;
|
|
602
|
+
}
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
function noteHoneyRouteHit(ip, now = Date.now()) {
|
|
606
|
+
const prev = routeHits.get(ip);
|
|
607
|
+
if (prev === void 0 || now - prev.start > ROUTE_WINDOW_MS) {
|
|
608
|
+
routeHits.set(ip, { count: 1, start: now });
|
|
609
|
+
return 1;
|
|
610
|
+
}
|
|
611
|
+
prev.count += 1;
|
|
612
|
+
return prev.count;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/runtime/rasp/middleware.ts
|
|
616
|
+
function raspContext() {
|
|
617
|
+
return function raspContextMiddleware(req, res, next) {
|
|
618
|
+
const ctx = contextFromRequest(req);
|
|
619
|
+
try {
|
|
620
|
+
const credHit = ctx.strings.some((s) => isHoneyCanary(s));
|
|
621
|
+
const routeHit = isHoneyRoute(ctx.path);
|
|
622
|
+
if (routeHit && !credHit) {
|
|
623
|
+
const count = noteHoneyRouteHit(ctx.ip);
|
|
624
|
+
addThreat(ctx.ip, "honey");
|
|
625
|
+
emitRaspEvent({
|
|
626
|
+
ruleId: "RASP-HONEY-001",
|
|
627
|
+
path: ctx.path,
|
|
628
|
+
ip: ctx.ip,
|
|
629
|
+
sample: "honeytoken",
|
|
630
|
+
severity: "low",
|
|
631
|
+
count
|
|
632
|
+
});
|
|
633
|
+
} else if (credHit) {
|
|
634
|
+
addThreat(ctx.ip, "honey");
|
|
635
|
+
emitRaspEvent({
|
|
636
|
+
ruleId: "RASP-HONEY-001",
|
|
637
|
+
path: ctx.path,
|
|
638
|
+
ip: ctx.ip,
|
|
639
|
+
sample: "canary-credential",
|
|
640
|
+
severity: "critical"
|
|
641
|
+
});
|
|
642
|
+
if (isIpBlocked(ctx.ip)) {
|
|
643
|
+
if (typeof res.status === "function") {
|
|
644
|
+
res.status(403).json({ error: "blocked", ruleId: "RASP-HONEY-001" });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
res.statusCode = 403;
|
|
648
|
+
res.end?.(JSON.stringify({ error: "blocked", ruleId: "RASP-HONEY-001" }));
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const proto = runDecide("proto", null, ctx);
|
|
653
|
+
if (proto.block && proto.ruleId) {
|
|
654
|
+
emitRaspEvent({
|
|
655
|
+
ruleId: proto.ruleId,
|
|
656
|
+
path: ctx.path,
|
|
657
|
+
ip: ctx.ip,
|
|
658
|
+
sample: "prototype-key"
|
|
659
|
+
});
|
|
660
|
+
if (raspModeFor(ctx.path, "proto") === "block") {
|
|
661
|
+
if (typeof res.status === "function") {
|
|
662
|
+
res.status(403).json({ error: "blocked", ruleId: proto.ruleId });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
res.statusCode = 403;
|
|
666
|
+
res.end?.(JSON.stringify({ error: "blocked", ruleId: proto.ruleId }));
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
} catch (err) {
|
|
671
|
+
if (err instanceof RaspBlockedError) {
|
|
672
|
+
if (typeof res.status === "function") {
|
|
673
|
+
res.status(403).json({ error: "blocked", ruleId: err.ruleId });
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
678
|
+
emitRaspEvent({
|
|
679
|
+
ruleId: RASP_FAIL_OPEN,
|
|
680
|
+
path: ctx.path,
|
|
681
|
+
ip: ctx.ip,
|
|
682
|
+
sample: "hook-error"
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
enterRaspContext(ctx, () => {
|
|
686
|
+
next();
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/runtime/rasp/hooks.ts
|
|
692
|
+
var require2 = createRequire(import.meta.url);
|
|
693
|
+
var restorers = [];
|
|
694
|
+
var wrappedPackages = /* @__PURE__ */ new WeakSet();
|
|
695
|
+
var hooksLive = false;
|
|
696
|
+
var sqliteHookRegistered = false;
|
|
697
|
+
function sampleOf(sink) {
|
|
698
|
+
if (typeof sink === "string") return sink.slice(0, 40);
|
|
699
|
+
if (sink === null || sink === void 0) return "";
|
|
700
|
+
return "[object]";
|
|
701
|
+
}
|
|
702
|
+
function inferPathBase(p) {
|
|
703
|
+
const ctx = currentRaspContext();
|
|
704
|
+
if (!ctx) return void 0;
|
|
705
|
+
for (const s of ctx.strings) {
|
|
706
|
+
if (!s) continue;
|
|
707
|
+
const idx = p.indexOf(s);
|
|
708
|
+
if (idx > 0) return p.slice(0, idx);
|
|
709
|
+
}
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
function guardSink(kind, sink, base) {
|
|
713
|
+
try {
|
|
714
|
+
const ctx = currentRaspContext();
|
|
715
|
+
if (ctx) {
|
|
716
|
+
const decision = runDecide(kind, sink, ctx, base);
|
|
717
|
+
if (decision.block && decision.ruleId) {
|
|
718
|
+
emitRaspEvent({
|
|
719
|
+
ruleId: decision.ruleId,
|
|
720
|
+
path: ctx.path,
|
|
721
|
+
ip: ctx.ip,
|
|
722
|
+
sample: sampleOf(sink)
|
|
723
|
+
});
|
|
724
|
+
addThreat(ctx.ip, "block");
|
|
725
|
+
if (raspModeFor(ctx.path, kind) === "block") throw new RaspBlockedError(decision.ruleId);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch (err) {
|
|
729
|
+
if (err instanceof RaspBlockedError) throw err;
|
|
730
|
+
const ctx = currentRaspContext();
|
|
731
|
+
emitRaspEvent({
|
|
732
|
+
ruleId: RASP_FAIL_OPEN,
|
|
733
|
+
path: ctx?.path ?? "/",
|
|
734
|
+
ip: ctx?.ip ?? "unknown",
|
|
735
|
+
sample: "hook-error"
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function applySink(kind, sink, base, original) {
|
|
740
|
+
guardSink(kind, sink, base);
|
|
741
|
+
return original();
|
|
742
|
+
}
|
|
743
|
+
function patchMethod(obj, key, wrapper) {
|
|
744
|
+
const record = obj;
|
|
745
|
+
const orig = record[key];
|
|
746
|
+
if (typeof orig !== "function") return;
|
|
747
|
+
const wrapped = wrapper(orig);
|
|
748
|
+
try {
|
|
749
|
+
Object.defineProperty(wrapped, "name", { value: orig.name });
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
record[key] = wrapped;
|
|
753
|
+
restorers.push(() => {
|
|
754
|
+
record[key] = orig;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
function wrapSqlMethod(obj, key) {
|
|
758
|
+
patchMethod(obj, key, (orig) => {
|
|
759
|
+
return function(sql, ...rest) {
|
|
760
|
+
const text = typeof sql === "string" ? sql : sql && typeof sql === "object" && "text" in sql && typeof sql.text === "string" ? sql.text : "";
|
|
761
|
+
return applySink("sql", text, void 0, () => orig.call(this, sql, ...rest));
|
|
762
|
+
};
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function wrapShellCommand(obj, key, shellAlways) {
|
|
766
|
+
patchMethod(obj, key, (orig) => {
|
|
767
|
+
return function(...args) {
|
|
768
|
+
const opts = args[1] && typeof args[1] === "object" ? args[1] : args[2] && typeof args[2] === "object" ? args[2] : void 0;
|
|
769
|
+
const useShell = shellAlways || opts?.shell === true;
|
|
770
|
+
if (!useShell) return orig.call(this, ...args);
|
|
771
|
+
const cmd = typeof args[0] === "string" ? args[0] : "";
|
|
772
|
+
return applySink("shell", cmd, void 0, () => orig.call(this, ...args));
|
|
773
|
+
};
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function wrapFsPath(obj, key) {
|
|
777
|
+
patchMethod(obj, key, (orig) => {
|
|
778
|
+
return function(pathArg, ...rest) {
|
|
779
|
+
const p = typeof pathArg === "string" ? pathArg : Buffer.isBuffer(pathArg) ? pathArg.toString() : pathArg instanceof URL ? pathArg.pathname : typeof pathArg === "number" || typeof pathArg === "boolean" ? String(pathArg) : "";
|
|
780
|
+
return applySink("path", p, inferPathBase(p), () => orig.call(this, pathArg, ...rest));
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
function checkEgress(url) {
|
|
785
|
+
const allow = raspEgressAllow();
|
|
786
|
+
if (!allow || allow.length === 0) return;
|
|
787
|
+
let host = "";
|
|
788
|
+
try {
|
|
789
|
+
host = new URL(url).hostname;
|
|
790
|
+
} catch {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
if (host && !egressAllowed(host, allow)) {
|
|
794
|
+
const ctx = currentRaspContext();
|
|
795
|
+
emitRaspEvent({
|
|
796
|
+
ruleId: "RASP-EGRESS-001",
|
|
797
|
+
path: ctx?.path ?? "/",
|
|
798
|
+
ip: ctx?.ip ?? "unknown",
|
|
799
|
+
sample: host.slice(0, 40)
|
|
800
|
+
});
|
|
801
|
+
if (ctx) addThreat(ctx.ip, "block");
|
|
802
|
+
if (raspModeFor(ctx?.path ?? "/", "ssrf") === "block") throw new RaspBlockedError("RASP-EGRESS-001");
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
function wrapSsrf(obj, key, urlOf) {
|
|
806
|
+
patchMethod(obj, key, (orig) => {
|
|
807
|
+
return function(...args) {
|
|
808
|
+
const url = urlOf(args);
|
|
809
|
+
checkEgress(url);
|
|
810
|
+
return applySink("ssrf", url, void 0, () => orig.call(this, ...args));
|
|
811
|
+
};
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
var WRAP_SQLITE = /* @__PURE__ */ Symbol.for("fullshield.rasp.wrapSqlite");
|
|
815
|
+
var SQLITE_WRAPPED = /* @__PURE__ */ Symbol.for("fullshield.rasp.sqliteWrapped");
|
|
816
|
+
var origModuleLoad;
|
|
817
|
+
function protoFrom(value) {
|
|
818
|
+
if (!value || typeof value !== "object") return void 0;
|
|
819
|
+
const proto = value.DatabaseSync?.prototype;
|
|
820
|
+
if (proto && typeof proto === "object") return proto;
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
function sqlitePrototype(hint) {
|
|
824
|
+
const fromHint = protoFrom(hint);
|
|
825
|
+
if (fromHint) return fromHint;
|
|
826
|
+
const getBuiltin = process.getBuiltinModule;
|
|
827
|
+
if (typeof getBuiltin === "function") {
|
|
828
|
+
try {
|
|
829
|
+
const fromBuiltin = protoFrom(getBuiltin.call(process, "node:sqlite") ?? getBuiltin.call(process, "sqlite"));
|
|
830
|
+
if (fromBuiltin) return fromBuiltin;
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (origModuleLoad) {
|
|
835
|
+
try {
|
|
836
|
+
return protoFrom(origModuleLoad("node:sqlite", void 0, false));
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return void 0;
|
|
841
|
+
}
|
|
842
|
+
function sqliteAlreadyLoaded() {
|
|
843
|
+
const list = process.moduleLoadList;
|
|
844
|
+
return Array.isArray(list) && list.some((entry) => /(?:^| )sqlite$/i.test(entry));
|
|
845
|
+
}
|
|
846
|
+
function wrapSqliteNow(hint) {
|
|
847
|
+
try {
|
|
848
|
+
const proto = sqlitePrototype(hint);
|
|
849
|
+
if (!proto || proto[SQLITE_WRAPPED]) return;
|
|
850
|
+
Object.defineProperty(proto, SQLITE_WRAPPED, { value: true, configurable: true });
|
|
851
|
+
wrapSqlMethod(proto, "exec");
|
|
852
|
+
wrapSqlMethod(proto, "prepare");
|
|
853
|
+
restorers.push(() => {
|
|
854
|
+
delete proto[SQLITE_WRAPPED];
|
|
855
|
+
});
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function wrapBuiltins() {
|
|
860
|
+
globalThis[WRAP_SQLITE] = wrapSqliteNow;
|
|
861
|
+
const cp = require2("node:child_process");
|
|
862
|
+
const cpAlias = require2("child_process");
|
|
863
|
+
for (const mod of /* @__PURE__ */ new Set([cp, cpAlias])) {
|
|
864
|
+
wrapShellCommand(mod, "exec", true);
|
|
865
|
+
wrapShellCommand(mod, "execSync", true);
|
|
866
|
+
wrapShellCommand(mod, "spawn", false);
|
|
867
|
+
wrapShellCommand(mod, "spawnSync", false);
|
|
868
|
+
wrapShellCommand(mod, "execFile", false);
|
|
869
|
+
wrapShellCommand(mod, "execFileSync", false);
|
|
870
|
+
}
|
|
871
|
+
const fsNames = [
|
|
872
|
+
"readFile",
|
|
873
|
+
"readFileSync",
|
|
874
|
+
"writeFile",
|
|
875
|
+
"writeFileSync",
|
|
876
|
+
"appendFile",
|
|
877
|
+
"appendFileSync",
|
|
878
|
+
"open",
|
|
879
|
+
"openSync",
|
|
880
|
+
"createReadStream",
|
|
881
|
+
"createWriteStream",
|
|
882
|
+
"stat",
|
|
883
|
+
"statSync",
|
|
884
|
+
"lstat",
|
|
885
|
+
"lstatSync",
|
|
886
|
+
"access",
|
|
887
|
+
"accessSync",
|
|
888
|
+
"readdir",
|
|
889
|
+
"readdirSync",
|
|
890
|
+
"unlink",
|
|
891
|
+
"unlinkSync",
|
|
892
|
+
"rm",
|
|
893
|
+
"rmSync"
|
|
894
|
+
];
|
|
895
|
+
const fs = require2("node:fs");
|
|
896
|
+
const fsAlias = require2("fs");
|
|
897
|
+
for (const mod of /* @__PURE__ */ new Set([fs, fsAlias])) {
|
|
898
|
+
for (const name of fsNames) wrapFsPath(mod, name);
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const fsp = require2("node:fs/promises");
|
|
902
|
+
for (const name of ["readFile", "writeFile", "appendFile", "open", "stat", "lstat", "access", "readdir", "unlink", "rm"]) {
|
|
903
|
+
wrapFsPath(fsp, name);
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
const http = require2("node:http");
|
|
908
|
+
const https = require2("node:https");
|
|
909
|
+
wrapSsrf(http, "request", urlFromHttpArgs);
|
|
910
|
+
wrapSsrf(http, "get", urlFromHttpArgs);
|
|
911
|
+
wrapSsrf(https, "request", urlFromHttpArgs);
|
|
912
|
+
wrapSsrf(https, "get", urlFromHttpArgs);
|
|
913
|
+
if (typeof globalThis.fetch === "function") {
|
|
914
|
+
const origFetch = globalThis.fetch;
|
|
915
|
+
globalThis.fetch = function(input, init) {
|
|
916
|
+
const start = fetchUrl(input);
|
|
917
|
+
checkEgress(start);
|
|
918
|
+
const mode = init?.redirect ?? "follow";
|
|
919
|
+
if (mode !== "follow") {
|
|
920
|
+
return applySink("ssrf", start, void 0, () => origFetch.call(this, input, init));
|
|
921
|
+
}
|
|
922
|
+
guardSink("ssrf", start);
|
|
923
|
+
return (async () => {
|
|
924
|
+
let current = start;
|
|
925
|
+
let nextInput = input;
|
|
926
|
+
for (let hop = 0; hop < 8; hop += 1) {
|
|
927
|
+
const res = await origFetch.call(this, nextInput, { ...init, redirect: "manual" });
|
|
928
|
+
if (res.status < 300 || res.status >= 400) return res;
|
|
929
|
+
const loc = res.headers.get("location");
|
|
930
|
+
if (!loc) return res;
|
|
931
|
+
current = new URL(loc, current).href;
|
|
932
|
+
guardSink("ssrf", current);
|
|
933
|
+
nextInput = current;
|
|
934
|
+
}
|
|
935
|
+
return origFetch.call(this, current, init);
|
|
936
|
+
})();
|
|
937
|
+
};
|
|
938
|
+
restorers.push(() => {
|
|
939
|
+
globalThis.fetch = origFetch;
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const vm = require2("node:vm");
|
|
943
|
+
for (const name of ["runInNewContext", "runInThisContext", "runInContext", "compileFunction"]) {
|
|
944
|
+
patchMethod(vm, name, (orig) => {
|
|
945
|
+
return function(code, ...rest) {
|
|
946
|
+
return applySink(
|
|
947
|
+
"sql",
|
|
948
|
+
typeof code === "string" ? code : "",
|
|
949
|
+
void 0,
|
|
950
|
+
() => orig.call(this, code, ...rest)
|
|
951
|
+
);
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
const Module = require2("node:module");
|
|
956
|
+
origModuleLoad = Module._load.bind(Module);
|
|
957
|
+
const origLoad = Module._load;
|
|
958
|
+
Module._load = function(request, parent, isMain) {
|
|
959
|
+
const exported = origLoad.call(this, request, parent, isMain);
|
|
960
|
+
try {
|
|
961
|
+
return wrapKnownPackage(String(request), exported);
|
|
962
|
+
} catch {
|
|
963
|
+
emitRaspEvent({
|
|
964
|
+
ruleId: RASP_FAIL_OPEN,
|
|
965
|
+
path: currentRaspContext()?.path ?? "/",
|
|
966
|
+
ip: currentRaspContext()?.ip ?? "unknown",
|
|
967
|
+
sample: "hook-error"
|
|
968
|
+
});
|
|
969
|
+
return exported;
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
restorers.push(() => {
|
|
973
|
+
Module._load = origLoad;
|
|
974
|
+
origModuleLoad = void 0;
|
|
975
|
+
});
|
|
976
|
+
const registerHooks = Module.registerHooks;
|
|
977
|
+
if (typeof registerHooks === "function" && !sqliteHookRegistered) {
|
|
978
|
+
sqliteHookRegistered = true;
|
|
979
|
+
registerHooks({
|
|
980
|
+
load(url, context, nextLoad) {
|
|
981
|
+
const result = nextLoad(url, context);
|
|
982
|
+
if (hooksLive && typeof url === "string" && (url === "node:sqlite" || url.startsWith("node:sqlite"))) {
|
|
983
|
+
wrapSqliteNow();
|
|
984
|
+
}
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
if (sqliteAlreadyLoaded()) wrapSqliteNow();
|
|
990
|
+
}
|
|
991
|
+
function wrapExpressLike(exported) {
|
|
992
|
+
if (typeof exported !== "function") return;
|
|
993
|
+
const orig = exported;
|
|
994
|
+
if (wrappedPackages.has(orig)) return;
|
|
995
|
+
function patched(...args) {
|
|
996
|
+
const app = orig.apply(this, args);
|
|
997
|
+
try {
|
|
998
|
+
if (app && typeof app.use === "function" && !app.__fullshieldRasp) {
|
|
999
|
+
app.use(raspContext());
|
|
1000
|
+
app.__fullshieldRasp = true;
|
|
1001
|
+
}
|
|
1002
|
+
} catch {
|
|
1003
|
+
emitRaspEvent({
|
|
1004
|
+
ruleId: RASP_FAIL_OPEN,
|
|
1005
|
+
path: "/",
|
|
1006
|
+
ip: "unknown",
|
|
1007
|
+
sample: "hook-error"
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
return app;
|
|
1011
|
+
}
|
|
1012
|
+
Object.assign(patched, orig);
|
|
1013
|
+
Object.setPrototypeOf(patched, orig);
|
|
1014
|
+
wrappedPackages.add(orig);
|
|
1015
|
+
exported.__fullshieldExpressPatch = patched;
|
|
1016
|
+
}
|
|
1017
|
+
function wrapKnownPackage(request, exported) {
|
|
1018
|
+
if (exported === null || typeof exported !== "object" && typeof exported !== "function") return exported;
|
|
1019
|
+
const obj = exported;
|
|
1020
|
+
if (wrappedPackages.has(obj)) return exported;
|
|
1021
|
+
const name = request.replace(/^node:/, "");
|
|
1022
|
+
if (name === "sqlite") {
|
|
1023
|
+
wrapSqliteNow(exported);
|
|
1024
|
+
return exported;
|
|
1025
|
+
}
|
|
1026
|
+
if (name === "pg" || name.endsWith("/pg")) {
|
|
1027
|
+
const pg = exported;
|
|
1028
|
+
if (pg.Client?.prototype) wrapSqlMethod(pg.Client.prototype, "query");
|
|
1029
|
+
if (pg.Pool?.prototype) wrapSqlMethod(pg.Pool.prototype, "query");
|
|
1030
|
+
wrappedPackages.add(obj);
|
|
1031
|
+
return exported;
|
|
1032
|
+
}
|
|
1033
|
+
if (name === "mysql2" || name.endsWith("/mysql2") || name.includes("mysql2/")) {
|
|
1034
|
+
const mysql = exported;
|
|
1035
|
+
const proto = mysql.Connection?.prototype ?? mysql.prototype;
|
|
1036
|
+
if (proto) {
|
|
1037
|
+
wrapSqlMethod(proto, "query");
|
|
1038
|
+
wrapSqlMethod(proto, "execute");
|
|
1039
|
+
}
|
|
1040
|
+
wrappedPackages.add(obj);
|
|
1041
|
+
return exported;
|
|
1042
|
+
}
|
|
1043
|
+
if (name === "better-sqlite3" || name.endsWith("/better-sqlite3")) {
|
|
1044
|
+
const proto = exported.prototype;
|
|
1045
|
+
if (proto) {
|
|
1046
|
+
wrapSqlMethod(proto, "exec");
|
|
1047
|
+
wrapSqlMethod(proto, "prepare");
|
|
1048
|
+
}
|
|
1049
|
+
wrappedPackages.add(obj);
|
|
1050
|
+
return exported;
|
|
1051
|
+
}
|
|
1052
|
+
if (name === "mongodb" || name.includes("mongodb")) {
|
|
1053
|
+
const col = exported.Collection?.prototype;
|
|
1054
|
+
if (col) {
|
|
1055
|
+
patchMethod(col, "find", (orig) => {
|
|
1056
|
+
return function(query, ...rest) {
|
|
1057
|
+
return applySink("nosql", query ?? {}, void 0, () => orig.call(this, query, ...rest));
|
|
1058
|
+
};
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
wrappedPackages.add(obj);
|
|
1062
|
+
return exported;
|
|
1063
|
+
}
|
|
1064
|
+
if (name === "mongoose" || name.includes("mongoose")) {
|
|
1065
|
+
const q = exported.Query?.prototype;
|
|
1066
|
+
if (q) {
|
|
1067
|
+
patchMethod(q, "find", (orig) => {
|
|
1068
|
+
return function(query, ...rest) {
|
|
1069
|
+
return applySink("nosql", query ?? {}, void 0, () => orig.call(this, query, ...rest));
|
|
1070
|
+
};
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
wrappedPackages.add(obj);
|
|
1074
|
+
return exported;
|
|
1075
|
+
}
|
|
1076
|
+
if (name === "axios") {
|
|
1077
|
+
const ax = exported;
|
|
1078
|
+
const urlOf = (args) => {
|
|
1079
|
+
const a = args[0];
|
|
1080
|
+
if (typeof a === "string") return a;
|
|
1081
|
+
if (a && typeof a === "object" && "url" in a && typeof a.url === "string") return a.url;
|
|
1082
|
+
return "";
|
|
1083
|
+
};
|
|
1084
|
+
wrapSsrf(ax, "request", urlOf);
|
|
1085
|
+
wrapSsrf(ax, "get", urlOf);
|
|
1086
|
+
wrapSsrf(ax, "post", urlOf);
|
|
1087
|
+
wrappedPackages.add(obj);
|
|
1088
|
+
return exported;
|
|
1089
|
+
}
|
|
1090
|
+
if (name === "express") {
|
|
1091
|
+
wrapExpressLike(exported);
|
|
1092
|
+
const patched = exported.__fullshieldExpressPatch;
|
|
1093
|
+
if (typeof patched === "function") {
|
|
1094
|
+
const resp = exported.response;
|
|
1095
|
+
if (resp && typeof resp.redirect === "function") {
|
|
1096
|
+
patchMethod(resp, "redirect", (orig) => {
|
|
1097
|
+
return function(...args) {
|
|
1098
|
+
const url = typeof args[0] === "string" ? args[0] : typeof args[1] === "string" ? args[1] : "";
|
|
1099
|
+
return applySink("ssrf", url, void 0, () => orig.call(this, ...args));
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
wrappedPackages.add(obj);
|
|
1104
|
+
return patched;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return exported;
|
|
1108
|
+
}
|
|
1109
|
+
function installHooks() {
|
|
1110
|
+
hooksLive = true;
|
|
1111
|
+
wrapBuiltins();
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/runtime/rasp/vpatch.ts
|
|
1115
|
+
function patchForcesBlock(patches, route, kind) {
|
|
1116
|
+
return patches.some((p) => {
|
|
1117
|
+
const routeHit = p.route === void 0 || p.route === route;
|
|
1118
|
+
const sinkHit = p.sink === void 0 || p.sink === kind;
|
|
1119
|
+
return routeHit && sinkHit;
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/runtime/rasp/enable.ts
|
|
1124
|
+
var enabled = false;
|
|
1125
|
+
var options = {};
|
|
1126
|
+
function raspMode() {
|
|
1127
|
+
return options.mode ?? "monitor";
|
|
1128
|
+
}
|
|
1129
|
+
function raspModeFor(route, kind) {
|
|
1130
|
+
if (patchForcesBlock(options.virtualPatches ?? [], route, kind)) return "block";
|
|
1131
|
+
return raspMode();
|
|
1132
|
+
}
|
|
1133
|
+
function raspEgressAllow() {
|
|
1134
|
+
return options.egress?.allow;
|
|
1135
|
+
}
|
|
1136
|
+
function runDecide(kind, sink, ctx, base) {
|
|
1137
|
+
if (options.decide) return options.decide(kind, sink, ctx);
|
|
1138
|
+
return decideSink(kind, sink, ctx, base);
|
|
1139
|
+
}
|
|
1140
|
+
function emitRaspEvent(event) {
|
|
1141
|
+
options.onEvent?.(event);
|
|
1142
|
+
}
|
|
1143
|
+
function enableRasp(opts = {}) {
|
|
1144
|
+
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_RASP !== "1") {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
options = { ...opts, experimental: true };
|
|
1148
|
+
configureThreat({ autoBlock: opts.threatAutoBlock === true, maxBlockMs: opts.threatMaxBlockMs });
|
|
1149
|
+
if (!enabled) {
|
|
1150
|
+
installHooks();
|
|
1151
|
+
enabled = true;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function enableRaspFromEnv() {
|
|
1155
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_RASP !== "1") return;
|
|
1156
|
+
enableRasp({
|
|
1157
|
+
experimental: true,
|
|
1158
|
+
mode: process.env.FULLSHIELD_RASP_MODE === "block" ? "block" : "monitor"
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/runtime/auto.ts
|
|
1163
|
+
enableRaspFromEnv();
|