progmune-runtime 3.7.26 → 3.7.27

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,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.27] — 2026-09-13
4
+
5
+ ### 归属校验 Python 化(A2,修复回归 fr-005 驱动)
6
+
7
+ - **跨用户资源写入检测**(`tools/extract_ir.py` 新标记 `__progmune_cross_user_write__`):函数把请求 payload 里的外来资源 id(folder_id)带着持久化(insert/add/create)却没有资源归属守卫 → 标记。**严格归属守卫词表**(任一出现即抑制):归属字段比较(user_id/owner_id/user.id)、`has_*/check_*_folder_access` 助手、`get_*_by_id_and_user_id` 限定查询——**不含 `has_permission`**(fr-005 真值函数含特性权限检查,与资源归属无关);`insert_` 前缀持久化原语不标(归属决策在上游路由层)
8
+ - **引擎 IR 层消费**(`src/trust/engine.ts`):`AUTHZ_CROSS_USER_WRITE` 违规直接报出(同 PATH_TRAVERSAL 模式);`protocol-detector.ts` 同名规则供 source-level 路径
9
+ - **fr-005 闭环验收**:修复前 DETECTED(open-webui main.py::chat_completion,真值位置);修复后 0 误报(has_folder_write_access 抑制);chats.py create_new_chat / automations.py(有守卫)均不标
10
+ - **修复回归战绩 2/8**(fr-007 路径穿越 TS 化 + fr-005 归属校验 Python 化)
11
+
12
+ ### 回归
13
+
14
+ - Python 盲测 64/64 零漂移 ✓ / Python 源级金标新规则 0 命中 ✓ / skyvern 24 无漂移 ✓ / 引擎测试 73 green ✓ / build ✓ / check 免疫正常 ✓
15
+
3
16
  ## [3.7.26] — 2026-09-13
4
17
 
5
18
  ### 精度修复批次(AI 生成项目验证 V2 + 真实修复回归驱动的引擎改动)
@@ -688,6 +688,20 @@ const SAFEGUARD_RULES = [
688
688
  conceptMissing: ["PathTraversalPrevention", "InputPathValidation"],
689
689
  conceptExpected: ["path allowlist", "basename normalization", "path sanitization"],
690
690
  },
691
+ {
692
+ name: "Authorization (Cross-User Resource Write)",
693
+ category: "authorization",
694
+ // 2026-09-13(REALWORLD_FIX_REGRESSION_V1 fr-005):Python 提取器在
695
+ // 「请求 payload 外来资源 id(folder_id)带着持久化、无归属守卫」时
696
+ // 注入标记。证据语义全部在提取器侧(严格归属守卫词表——不含
697
+ // has_permission 类特性权限检查,见 tools/extract_ir.py)。
698
+ languages: ["python"],
699
+ trigger: /\b(__progmune_cross_user_write__)\b/,
700
+ safeguards: [],
701
+ violationMessage: "Data is persisted with a user-supplied foreign resource id (folder_id) without an ownership guard — any authenticated user can write into another user's resource.",
702
+ conceptMissing: ["ResourceOwnership", "CrossUserReference"],
703
+ conceptExpected: ["folder write-access check", "get_*_by_id_and_user_id query"],
704
+ },
691
705
  {
692
706
  name: "XSS (Unsafe Template Rendering)",
693
707
  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.26";
23
+ exports.RUNTIME_VERSION = "3.7.27";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1265,6 +1265,7 @@ async function collectProtocolViolations(ctx, callGraph) {
1265
1265
  // 在 IR 层直接消费——call-sequence 构建会过滤 __progmune_ 前缀
1266
1266
  // (P4.5 历史),标记到不了 checkSpecificViolations;此前的 Python
1267
1267
  // 标记因此在引擎管线中一直是死的。此处同时点亮 Python 与 TS 两侧。
1268
+ // 2026-09-13(fr-005):同层消费跨用户资源写入标记。
1268
1269
  for (const f of functions) {
1269
1270
  const calls = f.calls || [];
1270
1271
  if (calls.includes("__progmune_path_traversal__")) {
@@ -1281,6 +1282,20 @@ async function collectProtocolViolations(ctx, callGraph) {
1281
1282
  policy_ref: "protocol-safety.specific",
1282
1283
  });
1283
1284
  }
1285
+ if (calls.includes("__progmune_cross_user_write__")) {
1286
+ violations.push({
1287
+ severity: "medium",
1288
+ rule_id: "AUTHZ_CROSS_USER_WRITE",
1289
+ file: f.file,
1290
+ function: f.name || "unknown",
1291
+ message: "Data is persisted with a user-supplied foreign resource id (folder_id) without an ownership guard — any authenticated user can write into another user's resource.",
1292
+ evidence: "__progmune_cross_user_write__",
1293
+ why: "Extractor-verified: folder_id from request payload is persisted (insert/add/create) " +
1294
+ "with no ownership comparison or folder write-access check in this function.",
1295
+ fix: "Verify folder write access for the acting user (ownership query or access-control helper) before persisting.",
1296
+ policy_ref: "protocol-safety.specific",
1297
+ });
1298
+ }
1284
1299
  }
1285
1300
  for (const f of functions) {
1286
1301
  if (!f.protocol)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.26",
3
+ "version": "3.7.27",
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/",
@@ -691,6 +691,47 @@ def has_template_tag_decorator(node):
691
691
  return False
692
692
 
693
693
 
694
+ CROSS_USER_WRITE_MARKER = "__progmune_cross_user_write__"
695
+
696
+
697
+ def has_cross_user_resource_write(node):
698
+ """跨用户资源写入(REALWORLD_FIX_REGRESSION_V1 fr-005 open-webui):
699
+ 函数把请求 payload 里的外来资源 id(folder_id 等)连同持久化调用
700
+ 写入(insert/add/create),却没有资源归属守卫——任意已认证用户可
701
+ 把数据写进其他用户的资源。归属守卫证据(任一出现即抑制):
702
+ ① 归属字段比较 user_id/owner_id/author_id/user.id/folder.user_id
703
+ ② 文件夹写权限助手 has_*/check_*_folder_access / has_folder_access
704
+ ③ 按用户 id 限定的查询 get_*_by_id_and_user_id。
705
+ 注意:不把 has_permission 算作归属证据——fr-005 真值函数含
706
+ features.direct_tool_servers 特性权限检查(user.role != 'admin' 同
707
+ 族,has_ownership_checked 会误放行),与资源归属无关。
708
+ 另:函数名以 insert_/save_/add_ 开头 = 持久化原语层(模型方法),
709
+ 归属决策在上游路由层,不标记。"""
710
+ try:
711
+ name = getattr(node, "name", "")
712
+ if re.match(r"^(insert_|save_|add_)", name):
713
+ return False
714
+ text = ast.unparse(node)
715
+ except Exception:
716
+ return False
717
+ if "folder_id" not in text:
718
+ return False
719
+ if not any(k in text for k in ("metadata", "form_data", "body", "request")):
720
+ return False
721
+ if not re.search(r"insert_new_chat|insert_new\w*|\binsert\(|\.add\(|\.create\(", text):
722
+ return False
723
+ if re.search(r"\b(user_id|owner_id|author_id|user\.id|folder\.user_id)\s*[!=]==?", text):
724
+ return False
725
+ if re.search(
726
+ r"has_folder_write_access|has_folder_access|"
727
+ r"has_\w*_folder_access|check_\w*_folder_access|check_folder\w*|"
728
+ r"get_\w+_by_id_and_user_id",
729
+ text,
730
+ ):
731
+ return False
732
+ return True
733
+
734
+
694
735
  def has_ownership_checked(node):
695
736
  """Inline ownership comparison: an identity-named parameter (user,
696
737
  current_user, profile, author, owner) compared with ==/!= against another
@@ -861,6 +902,8 @@ def extract_calls(node, unsafe_vars=None, imports=None, module_constants=None, g
861
902
  calls.append(TEMPLATE_TAG_MARKER)
862
903
  if has_ownership_checked(node) and OWNERSHIP_CHECKED_MARKER not in calls:
863
904
  calls.append(OWNERSHIP_CHECKED_MARKER)
905
+ if has_cross_user_resource_write(node) and CROSS_USER_WRITE_MARKER not in calls:
906
+ calls.append(CROSS_USER_WRITE_MARKER)
864
907
  if has_command_taint_flow(node) and CMD_FLOW_MARKER not in calls:
865
908
  calls.append(CMD_FLOW_MARKER)
866
909
  if has_csrf_exempt(node) and CSRF_MARKER not in calls: