progmune-runtime 3.7.58 → 3.7.60
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
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [3.7.
|
|
3
|
+
## [3.7.60] — 2026-10-05(未发布)
|
|
4
|
+
|
|
5
|
+
### fr-002/003 点亮:令牌交换路径检查完备性(修复回归 10/17)
|
|
6
|
+
|
|
7
|
+
**策略分支漏报根因第一刀**:OAuth 令牌交换路径绕过正常回调路径的检查——
|
|
8
|
+
open-webui 同一函数 `token_exchange` 的两个独立公告:缺角色策略判定
|
|
9
|
+
(fr-002,被角色策略拒绝的用户仍可登录)+ 缺域白名单检查(fr-003)。
|
|
10
|
+
|
|
11
|
+
- 提取器 `tools/extract_ir.py`:`has_token_exchange_missing_guard`——函数名
|
|
12
|
+
token exchange 形态 + 函数体(AST 全量文本)缺角色判定或缺域检查 ⇒
|
|
13
|
+
注入 `__progmune_token_exchange_unguarded__`。缺席证据来自 AST 全量
|
|
14
|
+
文本(非 TS 侧 calls 通道的盲判,R75 适用边界明确)
|
|
15
|
+
- 引擎规则 "Token Exchange Missing OAuth Guard"(python-only marker-driven)
|
|
16
|
+
- **三态验证**:fr-002 parent 报(role 缺)/ fr-002 fix 0(两项齐)/
|
|
17
|
+
fr-003 fix 报(该时间点 role 漏洞在场,正确);Python 盲测 90 项目
|
|
18
|
+
零触发(2533 条逐条一致);定向测试 8/8
|
|
19
|
+
- **fr-corpus 战绩:8 干净 DETECTED + 2 with-boundary = 有效 10/17**
|
|
20
|
+
- **fr-006 判定不可点亮**:SSRF 黑名单条目缺失是「配置/词表完备性」
|
|
21
|
+
知识问题,静态判据无法穷举「该有的条目」,不硬做。归入漏报根因
|
|
22
|
+
新类「配置/词表完备性」
|
|
23
|
+
|
|
24
|
+
### fr-014 点亮 + fr-015 关闭(2026-10-05)
|
|
4
25
|
|
|
5
26
|
### fr-001 点亮:身份映射子串匹配(修复回归 6/17)
|
|
6
27
|
|
|
@@ -22,7 +43,34 @@ external-ID)用 JSON 列 `.contains()` 做匹配——contains() 在 JSON 列
|
|
|
22
43
|
不可点亮:污点根是 DB 字段(跨请求存储污点),现有管线结构性够不着,
|
|
23
44
|
归入漏报根因新子类(详见 CAPABILITY_THREE_PROOFS_V1.md)。
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
### fr-014 点亮 + fr-015 关闭(2026-10-05)
|
|
47
|
+
|
|
48
|
+
- **fr-014(8/17,DETECTED-with-boundary)**:mockoon 管理 API 裸奔检出——
|
|
49
|
+
两处 Express 检测器可见性缺口修复:①`extractRoutes` 支持模板字符串路径
|
|
50
|
+
(库形态模块路径全是 `${prefix}/...` 变量拼前缀,此前整文件 0 路由);
|
|
51
|
+
②`app: Express` 形参的 setup 模块纳入整 app 无认证判定(此前落在
|
|
52
|
+
creator 门与 hasAnyAuth 门之间的盲区)。修复后残留归因到匿名中间件
|
|
53
|
+
函数体内的 bearer 校验超出名字级分类;AUTH_MIDDLEWARE_PATTERNS 补
|
|
54
|
+
timingSafeEqual/extractBearerToken(对具名形态有效)。express-realworld
|
|
55
|
+
回归 20 路由零新增误报。
|
|
56
|
+
- **fr-015 判定关闭(corpus_invalid)**:真值 sink 在虚拟模块
|
|
57
|
+
(#og-image-virtual/public-assets.mjs,pre 快照不存在),mini 切片无 sink;
|
|
58
|
+
且 pre-fix 已有 isBlockedUrl 守卫未接在 fonts 路径——fr-009 同族
|
|
59
|
+
「守卫存在但不足」。检测无从谈起,如实不覆盖。
|
|
60
|
+
|
|
61
|
+
### fr-012 复测点亮 + fr-004 判定关闭(2026-10-05)
|
|
62
|
+
|
|
63
|
+
- **fr-012(7/17,DETECTED-with-boundary)**:MCP 工具实参污点根(3.7.33 C 系列)
|
|
64
|
+
上线后修复前 markdownUpload 已命中路径穿越标记——此前未复测。修复后残留
|
|
65
|
+
2 条归因到 local 部署 by-design 分支(IS_REMOTE 拒绝 filePath 是修复,
|
|
66
|
+
local 保留文件读取是设计;提取器无分支语义,fr-009 同族边界)。
|
|
67
|
+
- **fr-004 判定关闭**:形态是闭包晚绑定(循环内 cookies 被内层函数晚绑定
|
|
68
|
+
捕获,多工具服务器时共享最后连接的 cookie jar)而非污点流——检测需变量
|
|
69
|
+
绑定分析,词形判据下 FP 爆炸,不硬做。根因归类修正为「作用域语义
|
|
70
|
+
(闭包晚绑定)」。注:fix_commit cd9db21c 是「重构形式的安全修复」
|
|
71
|
+
(fr-013 教训的镜像:fix 性质必须以公告摘要核对)。
|
|
72
|
+
|
|
73
|
+
## [3.7.57] — 2026-10-04(3.7.57~3.7.59 合并条目)
|
|
26
74
|
|
|
27
75
|
### §51 held-out 验证 + §52-53 决策门禁 + 修冤枉第一刀(Data Mutation 族移除)
|
|
28
76
|
|
|
@@ -76,6 +76,11 @@ const AUTH_MIDDLEWARE_PATTERNS = [
|
|
|
76
76
|
/\bensureAuthenticated\b/,
|
|
77
77
|
/\bauthGuard\b/,
|
|
78
78
|
/\bAuthGuard\b/,
|
|
79
|
+
// 匿名中间件函数体内的 bearer 校验证据(2026-10-05 fr-014 mockoon 修复形态:
|
|
80
|
+
// app.use(`${prefix}*`, (req,res,next) => { hasValidAdminApiToken(...) })——
|
|
81
|
+
// 认证在函数体里,中间件名不可见)
|
|
82
|
+
/\btimingSafeEqual\b/,
|
|
83
|
+
/\bextractBearerToken\b/,
|
|
79
84
|
/\brequire\w*[Aa]uth\b/,
|
|
80
85
|
/\bcheckAuth\b/,
|
|
81
86
|
/\bprotect\b/,
|
|
@@ -161,17 +166,22 @@ function extractRoutes(code, appName) {
|
|
|
161
166
|
for (const method of methods) {
|
|
162
167
|
// Pattern: app.get('/path', middleware1, middleware2, handler)
|
|
163
168
|
// or: router.post('/path', handler)
|
|
164
|
-
|
|
169
|
+
// 2026-10-05(fr-014 mockoon):路径也支持模板字符串
|
|
170
|
+
// app.get(`${adminApiPrefix}/events`, ...)——库形态模块(app 经参数传入)
|
|
171
|
+
// 的路径几乎全是变量拼前缀的模板串,字符串字面量 regex 整文件 0 路由。
|
|
172
|
+
// 捕获组:1=单引号 2=双引号 3=模板串;path 取三者首个非空。
|
|
173
|
+
const routeRegex = new RegExp(`\\b${receivers}\\.${method}\\s*\\(\\s*(?:'([^']+)'|\"([^\"]+)\"|\`([^\`]+)\`)\\s*,([^;]+)\\)`, "gi");
|
|
165
174
|
let match;
|
|
166
175
|
while ((match = routeRegex.exec(code)) !== null) {
|
|
167
|
-
const
|
|
176
|
+
const routePath = match[1] ?? match[2] ?? match[3] ?? "";
|
|
177
|
+
const rawArgs = match[4].trim();
|
|
168
178
|
const args = rawArgs.split(",").map(a => a.trim()).filter(a => a.length > 0);
|
|
169
179
|
// Last arg is the handler, everything before is middleware
|
|
170
180
|
const handler = args[args.length - 1] || "anonymous";
|
|
171
181
|
const middlewares = args.slice(0, -1);
|
|
172
182
|
routes.push({
|
|
173
183
|
method,
|
|
174
|
-
path:
|
|
184
|
+
path: routePath,
|
|
175
185
|
handler,
|
|
176
186
|
middlewares,
|
|
177
187
|
line: code.slice(0, match.index).split("\n").length,
|
|
@@ -274,9 +284,14 @@ function analyzeExpressApp(code) {
|
|
|
274
284
|
const hasSession = globalMiddleware.some(m => m.type === "session") || /\bsession\s*\(/.test(code);
|
|
275
285
|
// ── Generate Issues ──
|
|
276
286
|
// 整 app 无认证:仅真 app(实例化 express())且自身有非公开 mutation
|
|
277
|
-
// 路由时报——main.ts 只挂载路由模块(真实认证在 controllers
|
|
287
|
+
// 路由时报——main.ts 只挂载路由模块(真实认证在 controllers 内)不算裸。
|
|
288
|
+
// 2026-10-05(fr-014 mockoon):库形态 setup 模块(app: Express 经参数
|
|
289
|
+
// 传入、在调用方 app 上挂载)同样报——此前「非 creator 且无 auth」落在
|
|
290
|
+
// 两条判定路之间(整 app 报要求 creator、逐路由报要求 hasAnyAuth),
|
|
291
|
+
// 管理 API 裸奔的系统性盲区。
|
|
292
|
+
const appIsParam = /\bapp\s*:\s*Express\b/.test(code);
|
|
278
293
|
const hasNakedMutation = routes.some(nonPublicMutation);
|
|
279
|
-
if (!hasAnyAuth && appIsCreator && hasNakedMutation) {
|
|
294
|
+
if (!hasAnyAuth && (appIsCreator || appIsParam) && hasNakedMutation) {
|
|
280
295
|
issues.push({
|
|
281
296
|
severity: "critical",
|
|
282
297
|
rule: "EXPRESS_NO_AUTH_MIDDLEWARE",
|
|
@@ -137,3 +137,55 @@ async def get_oauth_subject_from_claim(claim, db=None):
|
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
|
+
(0, vitest_1.describe)("fr-002/003 token exchange missing guard(提取器标记)", () => {
|
|
141
|
+
const TK_MARKER = "__progmune_token_exchange_unguarded__";
|
|
142
|
+
(0, vitest_1.it)("修复前形态:token_exchange 缺角色判定 → 注入标记(fr-002)", () => {
|
|
143
|
+
write("auths.py", `
|
|
144
|
+
async def token_exchange(request, provider, token_data, db=None):
|
|
145
|
+
email = token_data.get('email', '').lower()
|
|
146
|
+
user = await Users.get_user_by_oauth_sub(provider, token_data.get('sub'), db)
|
|
147
|
+
return await create_session_response(request, user, db, source='oauth')
|
|
148
|
+
`);
|
|
149
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
150
|
+
const fn = ir.find((f) => f.name === "token_exchange");
|
|
151
|
+
(0, vitest_1.expect)(fn).toBeDefined();
|
|
152
|
+
(0, vitest_1.expect)(fn.calls).toContain(TK_MARKER);
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.it)("修复前形态:token_exchange 缺域检查 → 注入标记(fr-003)", () => {
|
|
155
|
+
write("auths.py", `
|
|
156
|
+
async def token_exchange(request, provider, token_data, db=None):
|
|
157
|
+
email = token_data.get('email', '').lower()
|
|
158
|
+
user = await Users.get_user_by_oauth_sub(provider, token_data.get('sub'), db)
|
|
159
|
+
role = await oauth_manager.get_user_role(user, user_data)
|
|
160
|
+
return await create_session_response(request, user, db, source='oauth')
|
|
161
|
+
`);
|
|
162
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
163
|
+
const fn = ir.find((f) => f.name === "token_exchange");
|
|
164
|
+
(0, vitest_1.expect)(fn.calls).toContain(TK_MARKER);
|
|
165
|
+
});
|
|
166
|
+
(0, vitest_1.it)("修复后形态:角色判定 + 域检查都有 → 无标记", () => {
|
|
167
|
+
write("auths.py", `
|
|
168
|
+
async def token_exchange(request, provider, token_data, db=None):
|
|
169
|
+
email = token_data.get('email', '').lower()
|
|
170
|
+
if '*' not in auth_manager_config.OAUTH_ALLOWED_DOMAINS and email.split('@')[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS:
|
|
171
|
+
raise HTTPException(status_code=403)
|
|
172
|
+
user = await Users.get_user_by_oauth_sub(provider, token_data.get('sub'), db)
|
|
173
|
+
user = await oauth_manager.update_user_from_oauth(request=request, user=user, user_data=user_data, provider=provider, token=token_data, db=db)
|
|
174
|
+
return await create_session_response(request, user, db, source='oauth')
|
|
175
|
+
`);
|
|
176
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
177
|
+
const fn = ir.find((f) => f.name === "token_exchange");
|
|
178
|
+
(0, vitest_1.expect)(fn).toBeDefined();
|
|
179
|
+
(0, vitest_1.expect)(fn.calls).not.toContain(TK_MARKER);
|
|
180
|
+
});
|
|
181
|
+
(0, vitest_1.it)("负例:非 token exchange 函数 → 无标记", () => {
|
|
182
|
+
write("utils.py", `
|
|
183
|
+
async def refresh_token(request, db=None):
|
|
184
|
+
return await create_session_response(request, None, db)
|
|
185
|
+
`);
|
|
186
|
+
const ir = (0, extract_ir_python_1.extractIRPython)(dir);
|
|
187
|
+
for (const f of ir) {
|
|
188
|
+
(0, vitest_1.expect)(f.calls).not.toContain(TK_MARKER);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -862,6 +862,22 @@ const SAFEGUARD_RULES = [
|
|
|
862
862
|
conceptMissing: ["ExactIdentityMatch", "SubscriptExtraction"],
|
|
863
863
|
conceptExpected: ["oauth[provider]['sub'] == sub", "scim[provider]['external_id'] == external_id"],
|
|
864
864
|
},
|
|
865
|
+
{
|
|
866
|
+
name: "Token Exchange Missing OAuth Guard",
|
|
867
|
+
category: "auth",
|
|
868
|
+
// 2026-10-05(REALWORLD_FIX_REGRESSION_V1 fr-002/fr-003):OAuth 令牌
|
|
869
|
+
// 交换路径绕过正常回调路径的检查——open-webui 的 token_exchange 函数
|
|
870
|
+
// 缺角色策略判定(fr-002)与域白名单检查(fr-003)两个独立公告。
|
|
871
|
+
// Python 提取器在「函数名 token exchange 形态 + 函数体(AST 全量)
|
|
872
|
+
// 缺角色判定或缺域检查」时注入标记。缺席证据来自 AST 全量文本,
|
|
873
|
+
// 非 TS 侧 calls 通道(R75 的适用范围:calls 有损的盲判不适用)。
|
|
874
|
+
languages: ["python"],
|
|
875
|
+
trigger: /\b(__progmune_token_exchange_unguarded__)\b/,
|
|
876
|
+
safeguards: [],
|
|
877
|
+
violationMessage: "OAuth token exchange endpoint is missing the authorization checks the normal OAuth callback enforces — role policy (get_user_role / OAUTH_ALLOWED_ROLES) and/or email domain allowlist (OAUTH_ALLOWED_DOMAINS). Users rejected by role policy or outside the domain allowlist can still sign in through the token exchange path.",
|
|
878
|
+
conceptMissing: ["RolePolicyOnTokenExchange", "DomainAllowlistOnTokenExchange"],
|
|
879
|
+
conceptExpected: ["get_user_role before session creation", "OAUTH_ALLOWED_DOMAINS check"],
|
|
880
|
+
},
|
|
865
881
|
{
|
|
866
882
|
name: "XSS (Unsafe Template Rendering)",
|
|
867
883
|
category: "xss",
|
package/dist/sdk.js
CHANGED
|
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
|
|
|
20
20
|
const protocol_knowledge_1 = require("./protocol-knowledge");
|
|
21
21
|
const evidence_repository_1 = require("./evidence-repository");
|
|
22
22
|
/** Runtime version — stable public identifier. Internal layers evolve underneath. */
|
|
23
|
-
exports.RUNTIME_VERSION = "3.7.
|
|
23
|
+
exports.RUNTIME_VERSION = "3.7.60";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.60",
|
|
4
4
|
"description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/",
|
package/tools/extract_ir.py
CHANGED
|
@@ -782,6 +782,32 @@ def has_identity_substring_match(node):
|
|
|
782
782
|
return False
|
|
783
783
|
|
|
784
784
|
|
|
785
|
+
TOKEN_EXCHANGE_UNGUARDED_MARKER = "__progmune_token_exchange_unguarded__"
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def has_token_exchange_missing_guard(node):
|
|
789
|
+
"""OAuth 令牌交换路径绕过正常回调路径的检查(REALWORLD_FIX_REGRESSION_V1
|
|
790
|
+
fr-002/fr-003 open-webui——同一函数 token_exchange 的两个独立公告):
|
|
791
|
+
fr-002 角色策略(OAUTH_ALLOWED_ROLES/ADMIN_ROLES)未在令牌交换路径执行,
|
|
792
|
+
被角色策略拒绝的用户仍可登录;fr-003 域白名单(OAUTH_ALLOWED_DOMAINS)
|
|
793
|
+
同样只在正常 callback 检查、token_exchange 跳过。
|
|
794
|
+
判据(语义定义,正向证据):函数名是 token exchange 形态,且函数体
|
|
795
|
+
(AST 全量文本)缺角色判定或缺域检查——Python 提取器有 AST 全量函数体,
|
|
796
|
+
缺席证据不是 TS 侧「calls 通道有损」的盲判(R75 的适用范围说明)。"""
|
|
797
|
+
try:
|
|
798
|
+
name = getattr(node, "name", "")
|
|
799
|
+
if not re.search(r"token[_\s]?exchange", name, re.I):
|
|
800
|
+
return False
|
|
801
|
+
text = ast.unparse(node)
|
|
802
|
+
except Exception:
|
|
803
|
+
return False
|
|
804
|
+
has_role = bool(re.search(r"get_user_role|update_user_from_oauth|determined_role", text))
|
|
805
|
+
has_domain = bool(
|
|
806
|
+
re.search(r"OAUTH_ALLOWED_DOMAINS|ALLOWED_DOMAINS|email\.split\([^)]*@", text)
|
|
807
|
+
)
|
|
808
|
+
return not has_role or not has_domain
|
|
809
|
+
|
|
810
|
+
|
|
785
811
|
def has_cross_user_resource_write(node):
|
|
786
812
|
"""跨用户资源写入(REALWORLD_FIX_REGRESSION_V1 fr-005 open-webui):
|
|
787
813
|
函数把请求 payload 里的外来资源 id(folder_id 等)连同持久化调用
|
|
@@ -995,6 +1021,8 @@ def extract_calls(node, unsafe_vars=None, imports=None, module_constants=None, g
|
|
|
995
1021
|
calls.append(CROSS_USER_WRITE_MARKER)
|
|
996
1022
|
if has_identity_substring_match(node) and IDENTITY_SUBSTRING_MARKER not in calls:
|
|
997
1023
|
calls.append(IDENTITY_SUBSTRING_MARKER)
|
|
1024
|
+
if has_token_exchange_missing_guard(node) and TOKEN_EXCHANGE_UNGUARDED_MARKER not in calls:
|
|
1025
|
+
calls.append(TOKEN_EXCHANGE_UNGUARDED_MARKER)
|
|
998
1026
|
if has_command_taint_flow(node) and CMD_FLOW_MARKER not in calls:
|
|
999
1027
|
calls.append(CMD_FLOW_MARKER)
|
|
1000
1028
|
if has_csrf_exempt(node) and CSRF_MARKER not in calls:
|