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.58] — 2026-10-04(未发布)
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
- ## [3.7.57] — 2026-10-04(未发布)
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
- const routeRegex = new RegExp(`\\b${receivers}\\.${method}\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*,([^;]+)\\)`, "gi");
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 rawArgs = match[2].trim();
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: match[1],
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.58";
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.58",
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/",
@@ -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: