progmune-runtime 3.7.59 → 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 +23 -2
- package/dist/protocol-detector-identity-substring.test.js +52 -0
- package/dist/protocol-detector.js +16 -0
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
- package/tools/extract_ir.py +28 -0
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
|
|
|
@@ -49,7 +70,7 @@ external-ID)用 JSON 列 `.contains()` 做匹配——contains() 在 JSON 列
|
|
|
49
70
|
(闭包晚绑定)」。注:fix_commit cd9db21c 是「重构形式的安全修复」
|
|
50
71
|
(fr-013 教训的镜像:fix 性质必须以公告摘要核对)。
|
|
51
72
|
|
|
52
|
-
## [3.7.57] — 2026-10-04
|
|
73
|
+
## [3.7.57] — 2026-10-04(3.7.57~3.7.59 合并条目)
|
|
53
74
|
|
|
54
75
|
### §51 held-out 验证 + §52-53 决策门禁 + 修冤枉第一刀(Data Mutation 族移除)
|
|
55
76
|
|
|
@@ -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:
|