agentshield-sdk 13.5.0 → 14.2.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.
@@ -7,10 +7,17 @@
7
7
  * Plugins are simple objects with a detect() method that returns an array
8
8
  * of threat findings. All detection runs locally — no data ever leaves
9
9
  * your environment.
10
+ *
11
+ * This module now includes IsolatedPluginSandbox which uses Node's built-in
12
+ * `vm` module to run untrusted plugin source code in a sanitized context
13
+ * with no access to process, fs, net, http, or child_process. It also
14
+ * enforces a preemptive timeout via vm.Script's `timeout` option.
10
15
  */
11
16
 
12
17
  const path = require('path');
13
18
  const fs = require('fs');
19
+ const vm = require('vm');
20
+ const crypto = require('crypto');
14
21
 
15
22
  // =========================================================================
16
23
  // HELPERS
@@ -27,13 +34,29 @@ const now = () => {
27
34
  return Date.now();
28
35
  };
29
36
 
37
+ // Try to raise the old-space memory cap a bit so tight infinite-loop plugins
38
+ // still get killed by the vm timeout (which is what provides the real bound).
39
+ try {
40
+ const v8 = require('v8');
41
+ if (typeof v8.setFlagsFromString === 'function') {
42
+ // Best-effort only; ignored silently if not permitted.
43
+ v8.setFlagsFromString('--max-old-space-size=4096');
44
+ }
45
+ } catch (_err) {
46
+ // v8 module missing or setFlagsFromString unavailable — silently skip.
47
+ }
48
+
30
49
  // =========================================================================
31
- // PLUGIN SANDBOX
50
+ // PLUGIN SANDBOX (legacy — kept for backward compatibility)
32
51
  // =========================================================================
33
52
 
34
53
  /**
35
54
  * Runs plugins with timeout protection and error isolation.
36
55
  * Prevents a misbehaving plugin from crashing the host agent.
56
+ *
57
+ * NOTE: This sandbox only times execution and catches errors. It does not
58
+ * isolate plugin code from the host process. Use IsolatedPluginSandbox for
59
+ * untrusted plugin source.
37
60
  */
38
61
  class PluginSandbox {
39
62
  /**
@@ -57,10 +80,6 @@ class PluginSandbox {
57
80
  let error = null;
58
81
 
59
82
  try {
60
- // Run detection synchronously with a time check after completion.
61
- // True preemptive timeout would require worker_threads, but for a
62
- // lightweight zero-dependency SDK we keep it simple: run, measure,
63
- // and flag if it exceeded the budget.
64
83
  const output = plugin.detect(text, options);
65
84
  const durationMs = now() - start;
66
85
 
@@ -82,6 +101,393 @@ class PluginSandbox {
82
101
  }
83
102
  }
84
103
 
104
+ // =========================================================================
105
+ // ISOLATED PLUGIN SANDBOX (vm-based, real isolation)
106
+ // =========================================================================
107
+
108
+ /**
109
+ * Whitelist of modules considered safe to expose to plugins via the
110
+ * restricted `require`. Anything else is blocked.
111
+ * @type {Set<string>}
112
+ */
113
+ const DEFAULT_SAFE_MODULES = new Set([
114
+ 'util'
115
+ ]);
116
+
117
+ /**
118
+ * Build a sanitized console that writes to a string buffer instead of stdout.
119
+ * @param {{buffer: string}} sink - Object whose `buffer` field gets appended to
120
+ * @returns {object} Console-like object
121
+ */
122
+ function makeSafeConsole(sink) {
123
+ const write = (level) => (...args) => {
124
+ const line = args.map((a) => {
125
+ if (typeof a === 'string') return a;
126
+ try { return JSON.stringify(a); } catch (_) { return String(a); }
127
+ }).join(' ');
128
+ sink.buffer += `[${level}] ${line}\n`;
129
+ };
130
+ return {
131
+ log: write('log'),
132
+ info: write('info'),
133
+ warn: write('warn'),
134
+ error: write('error'),
135
+ debug: write('debug')
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Real plugin sandbox using Node's `vm` module. Plugin source runs in a
141
+ * brand-new context with no access to the host globals or the require()
142
+ * function of the host process.
143
+ */
144
+ class IsolatedPluginSandbox {
145
+ /**
146
+ * @param {object} [options]
147
+ * @param {number} [options.timeoutMs=100] - Hard execution budget in ms (preemptive)
148
+ * @param {string[]} [options.allowRequire=[]] - Whitelist of module IDs the plugin may require()
149
+ */
150
+ constructor(options = {}) {
151
+ this.timeoutMs = options.timeoutMs || 100;
152
+ // Combine the default whitelist with any caller-supplied entries.
153
+ this.allowRequire = new Set([
154
+ ...DEFAULT_SAFE_MODULES,
155
+ ...(Array.isArray(options.allowRequire) ? options.allowRequire : [])
156
+ ]);
157
+ }
158
+
159
+ /**
160
+ * Build a safe restricted `require` function for the plugin context.
161
+ * @returns {function}
162
+ * @private
163
+ */
164
+ _makeSafeRequire() {
165
+ const allowed = this.allowRequire;
166
+ return function safeRequire(id) {
167
+ if (typeof id !== 'string' || id.length === 0) {
168
+ throw new Error('require(id) expects a non-empty string');
169
+ }
170
+ if (!allowed.has(id)) {
171
+ throw new Error(`[Agent Shield] require("${id}") blocked by sandbox. Allowed: ${[...allowed].join(', ') || '(none)'}`);
172
+ }
173
+ // Only absolute, unambiguous module names reach here.
174
+ // eslint-disable-next-line global-require
175
+ return require(id);
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Run untrusted plugin source. The source is expected to export a
181
+ * detect(text, options) function by assigning to `module.exports`
182
+ * or by assigning a top-level `detect` binding.
183
+ *
184
+ * @param {string} source - Plugin source code as a string
185
+ * @param {string} text - Text to scan
186
+ * @param {object} [options] - Options passed to detect()
187
+ * @returns {{results: Array, error: string|null, durationMs: number, consoleOutput: string}}
188
+ */
189
+ runSource(source, text, options = {}) {
190
+ if (typeof source !== 'string' || source.length === 0) {
191
+ return { results: [], error: 'Plugin source must be a non-empty string', durationMs: 0, consoleOutput: '' };
192
+ }
193
+
194
+ const sink = { buffer: '' };
195
+ const safeConsole = makeSafeConsole(sink);
196
+ const moduleShim = { exports: {} };
197
+
198
+ // Build a fresh object so the plugin cannot mutate the host's globals
199
+ // by poisoning the prototype chain from inside the context.
200
+ const sandboxGlobals = Object.create(null);
201
+
202
+ // Use vm.runInNewContext to realm-isolate the built-ins. Each call gets
203
+ // its own Object/Array/String/etc so that prototype pollution inside the
204
+ // plugin does NOT affect the host process.
205
+ const realm = vm.runInNewContext(`({
206
+ String, Number, Boolean, Array, Object, RegExp, Math, Date, JSON,
207
+ Map, Set, WeakMap, WeakSet, Error, TypeError, RangeError, SyntaxError,
208
+ Symbol, Promise
209
+ })`, Object.create(null), { timeout: this.timeoutMs });
210
+
211
+ sandboxGlobals.String = realm.String;
212
+ sandboxGlobals.Number = realm.Number;
213
+ sandboxGlobals.Boolean = realm.Boolean;
214
+ sandboxGlobals.Array = realm.Array;
215
+ sandboxGlobals.Object = realm.Object;
216
+ sandboxGlobals.RegExp = realm.RegExp;
217
+ sandboxGlobals.Math = realm.Math;
218
+ sandboxGlobals.Date = realm.Date;
219
+ sandboxGlobals.JSON = realm.JSON;
220
+ sandboxGlobals.Map = realm.Map;
221
+ sandboxGlobals.Set = realm.Set;
222
+ sandboxGlobals.WeakMap = realm.WeakMap;
223
+ sandboxGlobals.WeakSet = realm.WeakSet;
224
+ sandboxGlobals.Error = realm.Error;
225
+ sandboxGlobals.TypeError = realm.TypeError;
226
+ sandboxGlobals.RangeError = realm.RangeError;
227
+ sandboxGlobals.SyntaxError = realm.SyntaxError;
228
+ sandboxGlobals.Symbol = realm.Symbol;
229
+ sandboxGlobals.Promise = realm.Promise;
230
+
231
+ // Restricted surface for the plugin.
232
+ sandboxGlobals.console = safeConsole;
233
+ sandboxGlobals.require = this._makeSafeRequire();
234
+ sandboxGlobals.module = moduleShim;
235
+ sandboxGlobals.exports = moduleShim.exports;
236
+
237
+ // Plugin inputs are made available as globals for convenience.
238
+ sandboxGlobals.__text = text;
239
+ sandboxGlobals.__options = options;
240
+
241
+ // Create a sanitized `globalThis` / `global` that points at the same
242
+ // frozen-ish object so the plugin sees a self-consistent environment
243
+ // without being able to reach the host.
244
+ sandboxGlobals.globalThis = sandboxGlobals;
245
+ sandboxGlobals.global = sandboxGlobals;
246
+
247
+ // Create the context. Do NOT use codeGeneration: {strings: true} — we
248
+ // explicitly forbid eval/new Function inside the plugin.
249
+ const context = vm.createContext(sandboxGlobals, {
250
+ name: 'agent-shield-plugin-sandbox',
251
+ codeGeneration: { strings: false, wasm: false }
252
+ });
253
+
254
+ // Wrap source so that if the plugin assigns `detect = ...` without
255
+ // module.exports, we still find it.
256
+ const wrapped = `
257
+ (function() {
258
+ 'use strict';
259
+ ${source}
260
+ ;if (typeof module.exports === 'function') { return module.exports; }
261
+ if (module.exports && typeof module.exports.detect === 'function') { return module.exports.detect; }
262
+ if (typeof detect === 'function') { return detect; }
263
+ return null;
264
+ })()
265
+ `;
266
+
267
+ const start = now();
268
+ let script;
269
+ try {
270
+ script = new vm.Script(wrapped, { filename: 'plugin.js' });
271
+ } catch (err) {
272
+ return {
273
+ results: [],
274
+ error: `Plugin compile error: ${err.message || String(err)}`,
275
+ durationMs: now() - start,
276
+ consoleOutput: sink.buffer
277
+ };
278
+ }
279
+
280
+ let detectFn;
281
+ try {
282
+ detectFn = script.runInContext(context, { timeout: this.timeoutMs });
283
+ } catch (err) {
284
+ const durationMs = now() - start;
285
+ const msg = err && err.message ? err.message : String(err);
286
+ console.log(`[Agent Shield] Isolated plugin load failed: ${msg}`);
287
+ return { results: [], error: msg, durationMs, consoleOutput: sink.buffer };
288
+ }
289
+
290
+ if (typeof detectFn !== 'function') {
291
+ return {
292
+ results: [],
293
+ error: 'Plugin did not export a detect() function',
294
+ durationMs: now() - start,
295
+ consoleOutput: sink.buffer
296
+ };
297
+ }
298
+
299
+ // Invoke detect() inside the same context with its own timeout so that
300
+ // a tight infinite loop here is still killed.
301
+ const callScript = new vm.Script(
302
+ '__detect(__text, __options)',
303
+ { filename: 'plugin-invoke.js' }
304
+ );
305
+ context.__detect = detectFn;
306
+
307
+ let output;
308
+ try {
309
+ output = callScript.runInContext(context, { timeout: this.timeoutMs });
310
+ } catch (err) {
311
+ const durationMs = now() - start;
312
+ const msg = err && err.message ? err.message : String(err);
313
+ console.log(`[Agent Shield] Isolated plugin threw: ${msg}`);
314
+ return { results: [], error: msg, durationMs, consoleOutput: sink.buffer };
315
+ }
316
+
317
+ const durationMs = now() - start;
318
+ const results = Array.isArray(output) ? output : [];
319
+ return { results, error: null, durationMs, consoleOutput: sink.buffer };
320
+ }
321
+ }
322
+
323
+ // =========================================================================
324
+ // PLUGIN VERIFIER / SIGNING
325
+ // =========================================================================
326
+
327
+ /**
328
+ * Compute an HMAC-SHA256 signature for a plugin source string.
329
+ * @param {string} source - Plugin source code
330
+ * @param {string} key - HMAC secret
331
+ * @returns {string} Hex-encoded signature
332
+ */
333
+ function signPlugin(source, key) {
334
+ if (typeof source !== 'string') {
335
+ throw new TypeError('signPlugin: source must be a string');
336
+ }
337
+ if (typeof key !== 'string' || key.length === 0) {
338
+ throw new TypeError('signPlugin: key must be a non-empty string');
339
+ }
340
+ return crypto.createHmac('sha256', key).update(source, 'utf8').digest('hex');
341
+ }
342
+
343
+ /**
344
+ * Verify a plugin signature using HMAC-SHA256 with a constant-time compare.
345
+ * @param {string} source - Plugin source code
346
+ * @param {string} signature - Hex-encoded signature to check
347
+ * @param {string} key - HMAC secret
348
+ * @returns {boolean} true if the signature is valid
349
+ */
350
+ function verifyPluginSignature(source, signature, key) {
351
+ if (typeof source !== 'string' || typeof signature !== 'string' || typeof key !== 'string') {
352
+ return false;
353
+ }
354
+ let expected;
355
+ try {
356
+ expected = signPlugin(source, key);
357
+ } catch (_err) {
358
+ return false;
359
+ }
360
+ if (expected.length !== signature.length) return false;
361
+ try {
362
+ return crypto.timingSafeEqual(
363
+ Buffer.from(expected, 'hex'),
364
+ Buffer.from(signature, 'hex')
365
+ );
366
+ } catch (_err) {
367
+ return false;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Verifier for plugin signatures. If configured with a signing key, any
373
+ * unsigned or invalidly signed plugin is rejected.
374
+ */
375
+ class PluginVerifier {
376
+ /**
377
+ * @param {object} [options]
378
+ * @param {string} [options.signingKey] - HMAC secret. If omitted, all plugins pass.
379
+ * @param {boolean} [options.requireSignature=true] - Reject unsigned plugins when key is set
380
+ */
381
+ constructor(options = {}) {
382
+ this.signingKey = typeof options.signingKey === 'string' ? options.signingKey : null;
383
+ this.requireSignature = options.requireSignature !== false;
384
+ }
385
+
386
+ /**
387
+ * Returns true when this verifier was configured with a signing key.
388
+ * @returns {boolean}
389
+ */
390
+ isConfigured() {
391
+ return typeof this.signingKey === 'string' && this.signingKey.length > 0;
392
+ }
393
+
394
+ /**
395
+ * Verify a manifest + source bundle.
396
+ * @param {string} source - Plugin source code
397
+ * @param {PluginManifest|object} manifest - Plugin manifest (may contain .signature)
398
+ * @returns {{valid: boolean, reason: string|null}}
399
+ */
400
+ verify(source, manifest) {
401
+ if (!this.isConfigured()) {
402
+ return { valid: true, reason: null };
403
+ }
404
+ const signature = manifest && typeof manifest.signature === 'string' ? manifest.signature : '';
405
+ if (!signature) {
406
+ if (this.requireSignature) {
407
+ return { valid: false, reason: 'Plugin is unsigned but verifier requires a signature' };
408
+ }
409
+ return { valid: true, reason: null };
410
+ }
411
+ const ok = verifyPluginSignature(source, signature, this.signingKey);
412
+ return ok
413
+ ? { valid: true, reason: null }
414
+ : { valid: false, reason: 'Plugin signature does not match' };
415
+ }
416
+ }
417
+
418
+ // =========================================================================
419
+ // PLUGIN MANIFEST
420
+ // =========================================================================
421
+
422
+ /**
423
+ * Capability strings a plugin may declare. A plugin that only uses regex
424
+ * matching should declare ['read_text', 'regex_only'] so the host can
425
+ * decide what to trust it with.
426
+ */
427
+ const VALID_CAPABILITIES = new Set([
428
+ 'read_text',
429
+ 'regex_only',
430
+ 'read_options',
431
+ 'network', // explicit — almost always should NOT be granted
432
+ 'filesystem', // explicit — almost always should NOT be granted
433
+ 'require_modules'
434
+ ]);
435
+
436
+ /**
437
+ * Manifest schema helper for plugins. A manifest describes what the plugin
438
+ * is and what it needs access to.
439
+ */
440
+ class PluginManifest {
441
+ /**
442
+ * Validate a manifest object.
443
+ * @param {object} manifest
444
+ * @returns {{valid: boolean, errors: string[]}}
445
+ */
446
+ static validate(manifest) {
447
+ const errors = [];
448
+ if (!manifest || typeof manifest !== 'object') {
449
+ return { valid: false, errors: ['Manifest must be a non-null object'] };
450
+ }
451
+ if (typeof manifest.name !== 'string' || manifest.name.length === 0) {
452
+ errors.push('Manifest "name" must be a non-empty string');
453
+ }
454
+ if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
455
+ errors.push('Manifest "version" must be a non-empty string');
456
+ }
457
+ if (typeof manifest.author !== 'string' || manifest.author.length === 0) {
458
+ errors.push('Manifest "author" must be a non-empty string');
459
+ }
460
+ if (!Array.isArray(manifest.capabilities)) {
461
+ errors.push('Manifest "capabilities" must be an array of strings');
462
+ } else {
463
+ for (const cap of manifest.capabilities) {
464
+ if (typeof cap !== 'string' || !VALID_CAPABILITIES.has(cap)) {
465
+ errors.push(`Unknown capability: ${String(cap)}`);
466
+ }
467
+ }
468
+ }
469
+ if (manifest.signature !== undefined && typeof manifest.signature !== 'string') {
470
+ errors.push('Manifest "signature" must be a string if provided');
471
+ }
472
+ return { valid: errors.length === 0, errors };
473
+ }
474
+
475
+ /**
476
+ * Create a signed manifest by attaching an HMAC signature over `source`.
477
+ * @param {object} manifest - Base manifest (name, version, author, capabilities)
478
+ * @param {string} source - Plugin source code
479
+ * @param {string} key - HMAC secret
480
+ * @returns {object} A new manifest object with a `signature` field
481
+ */
482
+ static sign(manifest, source, key) {
483
+ const { valid, errors } = PluginManifest.validate({ ...manifest, capabilities: manifest.capabilities || [] });
484
+ if (!valid) {
485
+ throw new Error(`[Agent Shield] Cannot sign invalid manifest: ${errors.join('; ')}`);
486
+ }
487
+ return { ...manifest, signature: signPlugin(source, key) };
488
+ }
489
+ }
490
+
85
491
  // =========================================================================
86
492
  // PLUGIN TEMPLATE
87
493
  // =========================================================================
@@ -346,4 +752,14 @@ class PluginManager {
346
752
  // EXPORTS
347
753
  // =========================================================================
348
754
 
349
- module.exports = { PluginManager, PluginTemplate, PluginSandbox };
755
+ module.exports = {
756
+ PluginManager,
757
+ PluginTemplate,
758
+ PluginSandbox,
759
+ IsolatedPluginSandbox,
760
+ PluginVerifier,
761
+ PluginManifest,
762
+ signPlugin,
763
+ verifyPluginSignature,
764
+ VALID_CAPABILITIES
765
+ };
@@ -51,6 +51,18 @@ const KNOWN_BAD_SERVERS = Object.freeze({
51
51
  'flowise-unpatched': {
52
52
  reason: 'CVSS 10.0 RCE actively exploited (CVE-2025-59528)',
53
53
  severity: 'critical'
54
+ },
55
+ 'lmdeploy-unpatched': {
56
+ reason: 'SSRF via vision-language image loader, exploited within 12 hours (CVE-2026-33626)',
57
+ severity: 'high'
58
+ },
59
+ 'nginx-ui-mcp': {
60
+ reason: 'Auth bypass on MCP-integrated HTTP endpoints, actively exploited (CVE-2026-33032)',
61
+ severity: 'critical'
62
+ },
63
+ 'splunk-mcp-server': {
64
+ reason: 'Auth tokens logged in cleartext (CVE-2026-20205)',
65
+ severity: 'high'
54
66
  }
55
67
  });
56
68
 
@@ -207,6 +219,18 @@ const CVE_REGISTRY = Object.freeze({
207
219
  description: 'Code injection in Flowise MCP node (CVSS 10.0) allows remote code execution. 12,000-15,000 instances exposed. Actively exploited since April 6.',
208
220
  fix: 'Upgrade Flowise to >=3.0.6. Restrict access to MCP node.'
209
221
  },
222
+ {
223
+ cve: 'CVE-2026-40933',
224
+ severity: 'critical',
225
+ description: 'Flowise MCP Adapters authenticated RCE (CVSS 9.9). Unsafe stdio command serialization in MCP adapter enables OS command execution via npx -c.',
226
+ fix: 'Upgrade Flowise to >=3.1.0.'
227
+ },
228
+ {
229
+ cve: 'CVE-2026-41264',
230
+ severity: 'high',
231
+ description: 'Flowise CSV Agent prompt injection to RCE. LLM-generated Python script executed without sandbox. No auth required.',
232
+ fix: 'Upgrade Flowise to >=3.1.0.'
233
+ },
210
234
  {
211
235
  cve: 'CVE-2025-8943',
212
236
  severity: 'critical',
@@ -243,6 +267,122 @@ const CVE_REGISTRY = Object.freeze({
243
267
  description: 'OS command injection RCE in codebase-mcp.',
244
268
  fix: 'Upgrade codebase-mcp. Never pass unsanitized inputs to shell.'
245
269
  }
270
+ ],
271
+ 'lmdeploy': [
272
+ {
273
+ cve: 'CVE-2026-33626',
274
+ severity: 'high',
275
+ description: 'LMDeploy SSRF via vision-language image loader (CVSS 7.5). load_image() fetches arbitrary URLs, enabling port scanning, IMDS access (169.254.169.254), and internal service probing. Exploited within 12 hours of disclosure.',
276
+ fix: 'Upgrade LMDeploy to >=0.12.3. Block private IP ranges and cloud metadata endpoints in image URLs.'
277
+ }
278
+ ],
279
+ 'nginx-ui': [
280
+ {
281
+ cve: 'CVE-2026-33032',
282
+ severity: 'critical',
283
+ description: 'nginx-ui auth bypass on MCP-integrated HTTP endpoints (CVSS 9.8). Actively exploited.',
284
+ fix: 'Apply nginx-ui patch. Enable authentication on all MCP endpoints.'
285
+ }
286
+ ],
287
+ 'splunk-mcp-server': [
288
+ {
289
+ cve: 'CVE-2026-20205',
290
+ severity: 'high',
291
+ description: 'Splunk MCP Server logs session/auth tokens in cleartext in _internal index (CVSS 7.2).',
292
+ fix: 'Upgrade Splunk MCP Server to >=1.0.3. Audit _internal index for leaked tokens.'
293
+ }
294
+ ],
295
+ 'mcp-ruby-sdk': [
296
+ {
297
+ cve: 'CVE-2026-33946',
298
+ severity: 'medium',
299
+ description: 'MCP Ruby SDK session fixation in SSE stream allows hijacking of MCP protocol communications.',
300
+ fix: 'Upgrade MCP Ruby SDK to >=0.9.2.'
301
+ }
302
+ ],
303
+ 'magento2-dev-mcp': [
304
+ {
305
+ cve: 'CVE-2026-5603',
306
+ severity: 'high',
307
+ description: 'Command injection in @elgentos/magento2-dev-mcp via child_process.execAsync with unsanitized input.',
308
+ fix: 'Upgrade magento2-dev-mcp. Sanitize all inputs before passing to child_process.'
309
+ }
310
+ ],
311
+ 'semantic-kernel': [
312
+ {
313
+ cve: 'CVE-2026-25592',
314
+ severity: 'high',
315
+ description: 'Microsoft Semantic Kernel .NET SDK <1.71.0: prompt injection invokes arbitrary kernel functions leading to RCE on host process. Disclosed by MSRC May 7, 2026.',
316
+ fix: 'Upgrade Semantic Kernel .NET SDK to >=1.71.0. Validate function names in kernel.invoke() calls.'
317
+ },
318
+ {
319
+ cve: 'CVE-2026-26030',
320
+ severity: 'high',
321
+ description: 'Microsoft Semantic Kernel Python <1.39.4: same RCE primitive as CVE-2026-25592 but in Python SDK.',
322
+ fix: 'Upgrade Semantic Kernel Python to >=1.39.4.'
323
+ }
324
+ ],
325
+ 'fastgpt': [
326
+ {
327
+ cve: 'CVE-2026-42302',
328
+ severity: 'critical',
329
+ description: 'FastGPT agent-sandbox unauthenticated RCE (CVSS 9.8). code-server launched with --auth none on 0.0.0.0:8080. Published May 8, 2026.',
330
+ fix: 'Upgrade FastGPT to >=4.14.13. Never expose agent-sandbox endpoints without authentication.'
331
+ },
332
+ {
333
+ cve: 'CVE-2026-44284',
334
+ severity: 'high',
335
+ description: 'FastGPT SSRF in MCP tool URL handling. Crafted URLs reach internal services and cloud metadata endpoints.',
336
+ fix: 'Upgrade FastGPT. Apply Agent Shield mcp-guard SSRF firewall.'
337
+ },
338
+ {
339
+ cve: 'CVE-2026-42344',
340
+ severity: 'high',
341
+ description: 'FastGPT DNS rebinding bypasses isInternalAddress() check. Attacker hostname resolves to internal IP after initial validation.',
342
+ fix: 'Upgrade FastGPT. Pin DNS resolution per request and re-validate on connect.'
343
+ }
344
+ ],
345
+ 'cline-kanban': [
346
+ {
347
+ cve: 'CVE-2026-44211',
348
+ severity: 'high',
349
+ description: 'Cline Kanban Server Cross-Origin WebSocket Hijacking. Missing origin validation lets attackers inject prompts into running agent terminals.',
350
+ fix: 'Upgrade Cline. Validate Origin header on WebSocket upgrade requests.'
351
+ }
352
+ ],
353
+ 'azure-sre-agent': [
354
+ {
355
+ cve: 'CVE-2026-32173',
356
+ severity: 'high',
357
+ description: 'Azure SRE Agent exposed live command streams over unauthenticated WebSocket to any Entra ID user (CVSS 8.6).',
358
+ fix: 'Apply Azure patch. Restrict WebSocket access to authorized principals only.'
359
+ }
360
+ ],
361
+ 'crewai': [
362
+ {
363
+ cve: 'CVE-2026-44400',
364
+ severity: 'high',
365
+ description: 'CrewAI Code Interpreter default enabled allows prompt injection → RCE chain.',
366
+ fix: 'Upgrade CrewAI to latest. Disable Code Interpreter by default. Apply Agent Shield shieldCrewAI wrapper.'
367
+ },
368
+ {
369
+ cve: 'CVE-2026-44401',
370
+ severity: 'high',
371
+ description: 'CrewAI Code Interpreter SSRF via prompt-injected URLs.',
372
+ fix: 'Upgrade CrewAI. Block private IP ranges in tool URLs.'
373
+ },
374
+ {
375
+ cve: 'CVE-2026-44402',
376
+ severity: 'high',
377
+ description: 'CrewAI Code Interpreter file-read primitive via prompt injection.',
378
+ fix: 'Upgrade CrewAI. Restrict filesystem access in Code Interpreter sandbox.'
379
+ },
380
+ {
381
+ cve: 'CVE-2026-44403',
382
+ severity: 'high',
383
+ description: 'CrewAI agent task chain prompt injection leads to unauthorized tool invocation.',
384
+ fix: 'Upgrade CrewAI. Use Agent Shield intent-firewall to validate cross-task transitions.'
385
+ }
246
386
  ]
247
387
  });
248
388
 
@@ -378,6 +518,7 @@ class SupplyChainScanner {
378
518
  this._scanSchema(tool, findings);
379
519
  this._scanForSSRF(tool, findings);
380
520
  this._scanForClawHavoc(tool, findings);
521
+ this._scanConsentPhishing(tool, findings);
381
522
  }
382
523
 
383
524
  // Analyze escalation chains
@@ -687,6 +828,29 @@ class SupplyChainScanner {
687
828
  }
688
829
  }
689
830
 
831
+ /** @private - Consent phishing: detect tools whose description misrepresents capabilities (OWASP ASI09) */
832
+ _scanConsentPhishing(tool, findings) {
833
+ if (!tool || !tool.description || !tool.inputSchema) return;
834
+ const desc = String(tool.description).toLowerCase();
835
+ const schemaStr = JSON.stringify(tool.inputSchema).toLowerCase();
836
+
837
+ const READ_WORDS = ['read', 'get', 'fetch', 'list', 'view', 'show', 'display', 'search', 'query', 'lookup'];
838
+ const WRITE_INDICATORS = ['"url"', '"endpoint"', '"host"', '"target"', '"destination"', '"webhook"', '"callback"', 'http', '"command"', '"exec"', '"shell"', '"script"'];
839
+ const BENIGN_WORDS = ['save', 'update', 'create', 'write', 'delete', 'send', 'post', 'execute', 'run', 'upload'];
840
+
841
+ const descLooksReadOnly = READ_WORDS.some(w => desc.includes(w)) && !BENIGN_WORDS.some(w => desc.includes(w));
842
+ const schemaHasWriteCapability = WRITE_INDICATORS.some(w => schemaStr.includes(w));
843
+
844
+ if (descLooksReadOnly && schemaHasWriteCapability) {
845
+ findings.push({
846
+ type: 'consent_phishing',
847
+ severity: 'high',
848
+ message: `Tool "${tool.name || 'unknown'}" description implies read-only ("${desc.substring(0, 60)}...") but schema contains write/network parameters. Users may approve dangerous actions unknowingly.`,
849
+ recommendation: 'Tool descriptions must accurately reflect capabilities. If the tool sends data to URLs or executes commands, the description must say so explicitly.'
850
+ });
851
+ }
852
+ }
853
+
690
854
  /** @private */
691
855
  _scanSchema(tool, findings) {
692
856
  if (tool && tool.inputSchema && tool.inputSchema.additionalProperties === true) {