@dcrays/mobook-security-plugin 2.0.18 → 2.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/index.js +150 -1
- package/openclaw.plugin.json +6 -3
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { realpathSync } from "node:fs";
|
|
3
5
|
|
|
4
6
|
// ============================================================
|
|
5
7
|
// MoBook 安全防护插件 v2.0
|
|
@@ -280,6 +282,8 @@ function sanitizeParamsForLog(params) {
|
|
|
280
282
|
|
|
281
283
|
const FETCH_WRAPPED_KEY = Symbol.for("mobook-security-plugin.fetch-wrapped");
|
|
282
284
|
const ORIGINAL_FETCH_KEY = Symbol.for("mobook-security-plugin.original-fetch");
|
|
285
|
+
const UNDICI_WRAPPED_KEY = Symbol.for("mobook-security-plugin.undici-wrapped");
|
|
286
|
+
const UNDICI_RUNTIME_DEPS_KEY = "__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__";
|
|
283
287
|
|
|
284
288
|
// 保存原始 fetch
|
|
285
289
|
function saveOriginalFetch() {
|
|
@@ -539,6 +543,21 @@ function installFetchInterceptor(api) {
|
|
|
539
543
|
const reqHeaders = getMergedRequestHeaders(input, init);
|
|
540
544
|
const reqBodyText = await getRequestBodyText(input, init);
|
|
541
545
|
|
|
546
|
+
// ---- 请求侧检查:禁止调用指定大模型端点(环境变量白名单放行) ----
|
|
547
|
+
const BLOCKED_LLM_ENDPOINTS = [
|
|
548
|
+
"ai-clawbot.shuwenda.com",
|
|
549
|
+
];
|
|
550
|
+
if (process.env.MOBOOK_ALLOW_SHUWENDA !== "1") {
|
|
551
|
+
try {
|
|
552
|
+
const urlObj = new URL(url);
|
|
553
|
+
if (BLOCKED_LLM_ENDPOINTS.some(host => urlObj.hostname === host)) {
|
|
554
|
+
log(`[LLM-REQ-BLOCK #${thisCallId}] 禁止调用被封禁的大模型端点: ${urlObj.hostname}`, "WARN");
|
|
555
|
+
alert(`BLOCKED_LLM_ENDPOINT: ${url.slice(0, 200)}`);
|
|
556
|
+
const wantsSse = guessRequestWantsSse(url, reqHeaders, reqBodyText);
|
|
557
|
+
return makeBlockedResponse(wantsSse, "⚠️ 安全策略:该大模型端点已被禁止调用");
|
|
558
|
+
}
|
|
559
|
+
} catch {}
|
|
560
|
+
}
|
|
542
561
|
|
|
543
562
|
// ---- 请求侧检查:提示词注入检测(已禁用) ----
|
|
544
563
|
// const promptInjectionPatterns = [
|
|
@@ -613,15 +632,145 @@ function installFetchInterceptor(api) {
|
|
|
613
632
|
return true;
|
|
614
633
|
}
|
|
615
634
|
|
|
635
|
+
// ============================================================
|
|
636
|
+
// 5b. undici.fetch 劫持 — 兼容 openclaw 2026.6.11+ 的 LLM 调用路径
|
|
637
|
+
// openclaw 2026.6.11+ 使用 undici.fetch 而非 globalThis.fetch 发送 LLM 请求
|
|
638
|
+
// 通过注入 globalThis["__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__"] 让
|
|
639
|
+
// openclaw 的 loadUndiciRuntimeDeps() 使用被包装的 undici.fetch
|
|
640
|
+
// ============================================================
|
|
641
|
+
|
|
642
|
+
function installUndiciInterceptor(api) {
|
|
643
|
+
if (globalThis[UNDICI_WRAPPED_KEY]) {
|
|
644
|
+
log("undici 拦截器已安装(避免重复包装)", "DEBUG");
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
// 通过 createRequire 加载 openclaw 进程中的 undici 模块
|
|
650
|
+
const pluginRequire = createRequire(import.meta.url);
|
|
651
|
+
const argv1 = process.argv[1];
|
|
652
|
+
if (!argv1) {
|
|
653
|
+
log("⚠️ process.argv[1] 为空,无法定位 openclaw 安装路径,undici 拦截器未安装", "WARN");
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
const realPath = realpathSync(path.resolve(argv1));
|
|
657
|
+
const hostRequire = createRequire(realPath);
|
|
658
|
+
const undici = hostRequire("undici");
|
|
659
|
+
|
|
660
|
+
const originalUndiciFetch = undici.fetch;
|
|
661
|
+
if (!originalUndiciFetch) {
|
|
662
|
+
log("⚠️ undici.fetch 不可用,undici 拦截器未安装", "WARN");
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
let undiciCallId = 0;
|
|
667
|
+
|
|
668
|
+
const wrappedUndiciFetch = async function(input, init) {
|
|
669
|
+
const thisCallId = ++undiciCallId;
|
|
670
|
+
const url = getUrlFromFetchArgs(input);
|
|
671
|
+
|
|
672
|
+
// 只拦截 LLM 相关请求
|
|
673
|
+
const isLlmCall = url.includes("/chat/completions") || url.includes("/v1/chat") || url.includes("/completions");
|
|
674
|
+
if (!isLlmCall) {
|
|
675
|
+
return originalUndiciFetch(input, init);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const method = init?.method || (input instanceof Request ? input.method : "GET") || "GET";
|
|
679
|
+
const reqHeaders = getMergedRequestHeaders(input, init);
|
|
680
|
+
const reqBodyText = await getRequestBodyText(input, init);
|
|
681
|
+
|
|
682
|
+
log(`[UNDICI-LLM-REQ #${thisCallId}] ${method} ${url.slice(0, 120)}`, "DEBUG");
|
|
683
|
+
|
|
684
|
+
// ---- 请求侧检查:禁止调用指定大模型端点 ----
|
|
685
|
+
const BLOCKED_LLM_ENDPOINTS = ["ai-clawbot.shuwenda.com"];
|
|
686
|
+
if (process.env.MOBOOK_ALLOW_SHUWENDA !== "1") {
|
|
687
|
+
try {
|
|
688
|
+
const urlObj = new URL(url);
|
|
689
|
+
if (BLOCKED_LLM_ENDPOINTS.some(host => urlObj.hostname === host)) {
|
|
690
|
+
log(`[UNDICI-LLM-REQ-BLOCK #${thisCallId}] 禁止调用被封禁的大模型端点: ${urlObj.hostname}`, "WARN");
|
|
691
|
+
alert(`UNDICI_BLOCKED_LLM_ENDPOINT: ${url.slice(0, 200)}`);
|
|
692
|
+
const wantsSse = guessRequestWantsSse(url, reqHeaders, reqBodyText);
|
|
693
|
+
return makeBlockedResponse(wantsSse, "⚠️ 安全策略:该大模型端点已被禁止调用");
|
|
694
|
+
}
|
|
695
|
+
} catch {}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// ---- 放行请求,获取响应 ----
|
|
699
|
+
const fetchStart = Date.now();
|
|
700
|
+
let resp;
|
|
701
|
+
try {
|
|
702
|
+
resp = await originalUndiciFetch(input, init);
|
|
703
|
+
} catch (e) {
|
|
704
|
+
log(`[UNDICI-LLM-FETCH-ERR #${thisCallId}] ${e.message}`, "ERROR");
|
|
705
|
+
throw e;
|
|
706
|
+
}
|
|
707
|
+
const durationMs = Date.now() - fetchStart;
|
|
708
|
+
|
|
709
|
+
// ---- 响应侧检查:敏感信息脱敏 ----
|
|
710
|
+
const sse = isSseResponse(resp);
|
|
711
|
+
let respBody = "";
|
|
712
|
+
try {
|
|
713
|
+
respBody = await resp.clone().text();
|
|
714
|
+
} catch {
|
|
715
|
+
respBody = "[unreadable]";
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const respText = extractTextFromResponseBody(respBody, sse);
|
|
719
|
+
if (!respText) {
|
|
720
|
+
log(`[UNDICI-LLM-RESP #${thisCallId}] 空响应体,放行 | ${durationMs}ms`, "DEBUG");
|
|
721
|
+
return resp;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const { sanitized, matches } = sanitizeContent(respText);
|
|
725
|
+
if (matches.length > 0) {
|
|
726
|
+
alert(`UNDICI_LLM_RESPONSE_SANITIZED [RESP #${thisCallId}]: 脱敏类型=${matches.join(", ")} | 原文长度=${respText.length} | 脱敏后长度=${sanitized.length} | 耗时=${durationMs}ms`);
|
|
727
|
+
log(`[UNDICI-LLM-RESP-SANITIZE #${thisCallId}] sanitized ${matches.join(", ")} (${respText.length}→${sanitized.length} chars)`, "WARN");
|
|
728
|
+
|
|
729
|
+
const patchedBody = patchResponseBody(respBody, sse, sanitized);
|
|
730
|
+
const headers = new Headers(resp.headers);
|
|
731
|
+
headers.delete("content-length");
|
|
732
|
+
headers.delete("content-encoding");
|
|
733
|
+
return new Response(patchedBody, {
|
|
734
|
+
status: resp.status,
|
|
735
|
+
statusText: resp.statusText,
|
|
736
|
+
headers
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
log(`[UNDICI-LLM-RESP #${thisCallId}] 无敏感信息,放行 | ${durationMs}ms`, "DEBUG");
|
|
741
|
+
return resp;
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
// 注入到 globalThis,让 openclaw 的 loadUndiciRuntimeDeps() 使用包装版
|
|
745
|
+
globalThis[UNDICI_RUNTIME_DEPS_KEY] = {
|
|
746
|
+
Agent: undici.Agent,
|
|
747
|
+
EnvHttpProxyAgent: undici.EnvHttpProxyAgent,
|
|
748
|
+
ProxyAgent: undici.ProxyAgent,
|
|
749
|
+
FormData: undici.FormData,
|
|
750
|
+
fetch: wrappedUndiciFetch
|
|
751
|
+
};
|
|
752
|
+
globalThis[UNDICI_WRAPPED_KEY] = true;
|
|
753
|
+
|
|
754
|
+
log("✅ undici.fetch 拦截器已安装 — 兼容 openclaw 2026.6.11+ LLM 调用路径", "INFO");
|
|
755
|
+
return true;
|
|
756
|
+
} catch (e) {
|
|
757
|
+
log(`⚠️ undici 拦截器安装失败: ${e.message}`, "ERROR");
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
616
762
|
// ============================================================
|
|
617
763
|
// 插件注册
|
|
618
764
|
// ============================================================
|
|
619
765
|
export default function register(api) {
|
|
620
766
|
log("MoBook 安全防护插件 v2.0 启动");
|
|
621
767
|
|
|
622
|
-
// ---- 安装 Fetch
|
|
768
|
+
// ---- 安装 Fetch 拦截器(globalThis.fetch 劫持) ----
|
|
623
769
|
installFetchInterceptor(api);
|
|
624
770
|
|
|
771
|
+
// ---- 安装 undici.fetch 拦截器(兼容 openclaw 2026.6.11+) ----
|
|
772
|
+
installUndiciInterceptor(api);
|
|
773
|
+
|
|
625
774
|
// ---- Hook 1: before_tool_call(命令拦截 + 审计) ----
|
|
626
775
|
api.on("before_tool_call", async (event, ctx) => {
|
|
627
776
|
const toolName = event?.toolName || "unknown";
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mobook-security-plugin",
|
|
3
3
|
"name": "MoBook 安全防护插件",
|
|
4
|
-
"version": "2.0
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"description": "MoBook 企业级安全插件 - 危险命令分级拦截、敏感信息全渠道脱敏(身份证GB11643/银行卡Luhn校验)、LLM响应拦截、操作审计",
|
|
6
6
|
"entry": "./index.js",
|
|
7
7
|
"type": "plugin",
|
|
8
|
-
"configSchema": {}
|
|
9
|
-
|
|
8
|
+
"configSchema": {},
|
|
9
|
+
"activation": {
|
|
10
|
+
"onStartup": true
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcrays/mobook-security-plugin",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MoBook 企业级安全插件 - 危险命令分级拦截、敏感信息全渠道脱敏(身份证GB11643/银行卡Luhn校验)、LLM响应拦截、操作审计",
|
|
6
6
|
"main": "index.js",
|
|
@@ -20,4 +20,4 @@
|
|
|
20
20
|
"./index.js"
|
|
21
21
|
]
|
|
22
22
|
}
|
|
23
|
-
}
|
|
23
|
+
}
|