progmune-runtime 3.7.53 → 3.7.54
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,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.54] — 2026-09-27
|
|
4
|
+
|
|
5
|
+
### §47 签名级会话证据 + §48 输入校验族摸底(含两条判据被数据否决)
|
|
6
|
+
|
|
7
|
+
**落地(§47)**:`SafeguardRule` 新增 `paramTypeSafeguards` 通道,`SESSION_BEARING_TYPE_RE
|
|
8
|
+
= /(?:^|[A-Za-z])Session$/i`。语义是**正向**证据且取自 AST 签名——函数手里握着一个
|
|
9
|
+
session 对象 ⇒ 「任何人无需凭证即可访问/修改」不成立。**只挂** Unauthenticated
|
|
10
|
+
Access / Mutation 两条,**不挂** Ownership:有会话 ≠ 操作对象属于自己,一并补上会让这一刀
|
|
11
|
+
变成洗白越权的工具(否命题由 fixture 反向②承担)。
|
|
12
|
+
|
|
13
|
+
选择「参数类型」而不是「调用列表」的理由:fp-pool 切片里入边为 0 的函数占 36%~42%,
|
|
14
|
+
凡是依赖 `calls` 的机制在那些函数上都会**静默失效**——表现为一切正常,实际没跑。
|
|
15
|
+
|
|
16
|
+
**实测**:真实池 LOST 4 / ADDED 0(全部 hedgedoc 的 `RequestWithSession`,逐条读源码确认
|
|
17
|
+
FP);合成池 LOST 3 / ADDED 0(全部本轮新补的 `webshape_H`,webshape 期望 66→72)。
|
|
18
|
+
|
|
19
|
+
**诚实分母(R77)**:24 条确认真漏洞当前**无一条**触发授权族规则 ⇒ 「24/24 仍报」对这一族
|
|
20
|
+
是平凡成立。本轮安全性实际由合成正反向 fixture + 被压 4 条逐行读源码承担。
|
|
21
|
+
|
|
22
|
+
**否决(§47.2 / §48)**:
|
|
23
|
+
- 「calls 里找不到写入 ⇒ 不报未认证变更」——缺席型判据要求「看不到 == 不存在」,但 calls
|
|
24
|
+
通道有损,提取失败会被伪装成「真的不写」⇒ 真漏洞静默消失且 gold 查不出来(**R75**)
|
|
25
|
+
- 「参数类型以 Options 收尾 ⇒ 内部打包、非外部输入」——`createUser(options)` 这类反例太常见,
|
|
26
|
+
且语料里没有反向 fixture。**现有真值可分 ≠ 判据安全**(**R79**)
|
|
27
|
+
- 附带修正工具缺陷:离线重放对 `requireMarker` 类规则必然短路(gold 的 calls 里没有
|
|
28
|
+
`__progmune_input_effect__`)⇒ 此前「40 条已压掉」是**假收益**(**R78**)
|
|
29
|
+
|
|
30
|
+
**§48 摸底结论**:输入校验族 12 条真漏洞 12/12 仍报(首个可证伪基线);可打误报 21 条,
|
|
31
|
+
但两族共同的瓶颈是**分不清参数是否外部输入**——这需要跨函数数据流,而入边 0 占 36%~42%。
|
|
32
|
+
⇒ 剩下 35 条误报(授权 14 + 校验 21)不是判据调得不够细,是**能力上限**。
|
|
33
|
+
|
|
34
|
+
**其它**:修正 `extract-ir.ts` 一处过期注释(写着「Input Validation 不接任何标记」,实际早已
|
|
35
|
+
接通)——注释说通道断着、实际已通,比没注释更危险。
|
|
36
|
+
|
|
37
|
+
**门**:tsc 0 错 / webshape 72-0 / taintpath 160-0 / fr-007 pre5·post0 + fr-016 pre7·post0 /
|
|
38
|
+
定向 9 文件 130 条全绿 / 反向验证:摘刀重跑 webshape_H 的 3 条 suppress 组精确转红
|
|
39
|
+
|
|
3
40
|
## [3.7.53] — 2026-09-26
|
|
4
41
|
|
|
5
42
|
### 授权族误报压降收口 + 三处可见性基建(§35-§46 合并发布)
|
package/dist/extract-ir.js
CHANGED
|
@@ -416,8 +416,14 @@ const PATH_TRAVERSAL_MARKER = "__progmune_path_traversal__";
|
|
|
416
416
|
// __progmune_auth_machinery__,TS 侧此前从未产出 ⇒ 规则侧零改动。
|
|
417
417
|
//
|
|
418
418
|
// 边界(写死在这里,别想当然):只有**接受标记的规则**会受益。
|
|
419
|
-
//
|
|
420
|
-
//
|
|
419
|
+
//
|
|
420
|
+
// ⚠ 2026-09-27 更正(别删这段):本注释此前长期写着
|
|
421
|
+
// 「Input Validation 的 safeguard 不接受任何 __progmune_* 标记,装饰器对它无效」。
|
|
422
|
+
// **该结论已失效** —— 本文件 E3 的 __progmune_input_schema__ 通道现已接通,
|
|
423
|
+
// 见 protocol-detector.ts:647 的 input_validation safeguard(含
|
|
424
|
+
// __progmune_input_schema__|__progmune_input_guard__)。
|
|
425
|
+
// 保留这条更正,是因为「注释说通道断着、实际早已通」比没有注释更危险:
|
|
426
|
+
// 它会让人把瓶颈判错地方(2026-09-27 摸底时差点据此重开一条已修好的通道)。
|
|
421
427
|
// ═══════════════════════════════════════════════════════════════
|
|
422
428
|
const AUTH_MACHINERY_MARKER = "__progmune_auth_machinery__";
|
|
423
429
|
/** 显式免鉴权装饰器 —— 出现即不注入。把公开端点判成「有鉴权」是最危险的假阴性。 */
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* §47:签名级会话证据 —— 参数类型以 `Session` 结尾 ⇒ 「未认证」结论不成立。
|
|
5
|
+
*
|
|
6
|
+
* 为什么不是继续在 calls 里找 auth 词:hedgedoc 的 pending-user 三个接口 +
|
|
7
|
+
* OidcController.callback 都是 `@UseGuards(SessionGuard)` 保护的会话类路由,
|
|
8
|
+
* 但它们**一个认证函数都不调用** —— 身份是从 `request.session.*` 上取的,
|
|
9
|
+
* 调用列表里只有 property access,选择器读不到。真实池里这四条全是人工确认的 FP。
|
|
10
|
+
*
|
|
11
|
+
* 改用**参数类型**:签名上存在 session 对象 ⇒ 这次调用必然落在某个会话上下文里。
|
|
12
|
+
* 好处是它来自 AST 签名,不依赖调用图连通(fp-pool 入边 0 的函数占 36%~42%,
|
|
13
|
+
* 依赖 calls 的机制在那里会静默失效)。
|
|
14
|
+
*
|
|
15
|
+
* ⚠ R66 的否命题在本文件 §47 反向② 组:
|
|
16
|
+
* 有会话 ≠ 操作对象属于自己。越权读取的同形状反例必须 Unauthenticated 熄灭
|
|
17
|
+
* 而 **Ownership 继续亮**,否则这一刀就是把越权漏洞洗白。
|
|
18
|
+
*/
|
|
19
|
+
const vitest_1 = require("vitest");
|
|
20
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
21
|
+
const ACC = "Authorization (Unauthenticated Access)";
|
|
22
|
+
const MUT = "Authorization (Unauthenticated Mutation)";
|
|
23
|
+
const OWN = "Authorization (Ownership Check)";
|
|
24
|
+
const RES = "Authorization (Resource Ownership)";
|
|
25
|
+
/** lang 固定 typescript;params/paramTypes 逐用例给定 */
|
|
26
|
+
function fire(calls, name, params, paramTypes) {
|
|
27
|
+
const vs = (0, protocol_detector_1.detectSafeguardViolations)(calls, name, "typescript", params, false, paramTypes);
|
|
28
|
+
return new Set(vs.map((v) => v.rule));
|
|
29
|
+
}
|
|
30
|
+
const lit = (b) => (b ? "fires" : "silent");
|
|
31
|
+
(0, vitest_1.describe)("§47 正向:签名带会话 ⇒ Unauthenticated 两条熄灭(hedgedoc 真实 FP)", () => {
|
|
32
|
+
(0, vitest_1.it)("getPendingUserData(request: RequestWithSession) 不再报未认证读取", () => {
|
|
33
|
+
const s = fire(["create"], "getPendingUserData", ["request"], ["RequestWithSession"]);
|
|
34
|
+
(0, vitest_1.expect)(lit(s.has(ACC))).toBe("silent");
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)("confirmPendingUserData(request: RequestWithSession, dto) 不再报未认证变更", () => {
|
|
37
|
+
const s = fire(["createUserWithIdentityFromPendingUserConfirmation", "save"], "confirmPendingUserData", ["request", "pendingUserConfirmationData"], ["RequestWithSession", "PendingUserConfirmationDto"]);
|
|
38
|
+
(0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("OidcController.callback —— session 不在第一个参数也算数", () => {
|
|
41
|
+
const s = fire(["extractUserInfoFromCallback", "getExistingOidcIdentity", "mayUpdateIdentity", "save"], "OidcController.callback", ["oidcIdentifier", "request"], ["string", "RequestWithSession"]);
|
|
42
|
+
(0, vitest_1.expect)(lit(s.has(ACC))).toBe("silent");
|
|
43
|
+
(0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
|
|
44
|
+
});
|
|
45
|
+
(0, vitest_1.it)("裸 Session 类型同样成立(不要求 Request 前缀)", () => {
|
|
46
|
+
const s = fire(["find"], "getProfile", ["session"], ["Session"]);
|
|
47
|
+
(0, vitest_1.expect)(lit(s.has(ACC))).toBe("silent");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.describe)("§47 反向①:摘掉这一刀必须重新亮(证明是它在压,不是别机制抢先)", () => {
|
|
51
|
+
(0, vitest_1.it)("同函数把 RequestWithSession 换成 Request ⇒ 未认证读取重新报", () => {
|
|
52
|
+
const s = fire(["create"], "getPendingUserData", ["request"], ["Request"]);
|
|
53
|
+
(0, vitest_1.expect)(lit(s.has(ACC))).toBe("fires");
|
|
54
|
+
});
|
|
55
|
+
// 注意这里**不是** §47 在压:`request` 的类型退化成标量后,是 §44 的 paramGated
|
|
56
|
+
// 先把规则整体跳过了(标量参数不算身份证据)。单独留一条是为了把两层闸门的关系钉住:
|
|
57
|
+
// 类型通道同时喂给 paramGate(§44)和 paramTypeSafeguards(§47),
|
|
58
|
+
// 断言这一条可以防止以后有人误以为「类型一变就该亮」。
|
|
59
|
+
(0, vitest_1.it)("request:string 时由 §44 paramGate 先行跳过 —— 记录两层闸门的先后关系", () => {
|
|
60
|
+
const s = fire(["createUserWithIdentityFromPendingUserConfirmation", "save"], "confirmPendingUserData", ["request", "pendingUserConfirmationData"], ["string", "PendingUserConfirmationDto"]);
|
|
61
|
+
(0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)("不传类型通道时退回旧行为(仍报)—— R57 要求通道不可用时不得静默压", () => {
|
|
64
|
+
const vs = (0, protocol_detector_1.detectSafeguardViolations)(["create"], "getPendingUserData", "typescript", ["request"]);
|
|
65
|
+
(0, vitest_1.expect)(lit(vs.some((v) => v.rule === ACC))).toBe("fires");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
(0, vitest_1.describe)("§47 反向②:有会话 ≠ 有权 —— 越权形态的 Unauthenticated 熄灭但 Ownership 必须继续亮", () => {
|
|
69
|
+
(0, vitest_1.it)("removeSharedFile(req: RequestWithSession, id) —— 归属检查不能随会话一起被免掉", () => {
|
|
70
|
+
const s = fire(["findById", "removeFile"], "removeSharedFile", ["request", "id"], ["RequestWithSession", "string"]);
|
|
71
|
+
(0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
|
|
72
|
+
(0, vitest_1.expect)(lit(s.has(OWN))).toBe("fires");
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.it)("deleteOtherUsersNote(session: Session, noteId) —— Resource Ownership 必须继续亮", () => {
|
|
75
|
+
const s = fire(["findNote", "deleteNote"], "deleteOtherUsersNote", ["session", "noteId"], ["Session", "string"]);
|
|
76
|
+
(0, vitest_1.expect)(lit(s.has(MUT))).toBe("silent");
|
|
77
|
+
(0, vitest_1.expect)(lit(s.has(OWN) || s.has(RES))).toBe("fires");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.describe)("§47 边界:类型名不得以 Session 收尾就被误当成会话", () => {
|
|
81
|
+
const cases = [
|
|
82
|
+
["Transaction", " mutation"],
|
|
83
|
+
["SessionOptions", "配置对象不是会话本身"],
|
|
84
|
+
["Partial<AuthConf>", "verdaccio addAuth 的真类型"],
|
|
85
|
+
["Knex", "查询构造器"],
|
|
86
|
+
];
|
|
87
|
+
for (const [type, why] of cases) {
|
|
88
|
+
(0, vitest_1.it)(`${type} 不应被当作会话(${why.trim()})`, () => {
|
|
89
|
+
const s = fire(["merge"], "addAuth", ["auth"], [type]);
|
|
90
|
+
(0, vitest_1.expect)(lit(s.has(MUT))).toBe("fires");
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
@@ -314,6 +314,22 @@ exports.AUTHZ_BARE_CAN_RE = /^(?:can|cannot)$|\.(?:can|cannot)$/i;
|
|
|
314
314
|
* ⇒ `canSendEmail` 会拆出单词 "can" ⇒ 裸 can 误命中 ⇒ §38 的负对照直接转绿。
|
|
315
315
|
* callsOnly 把匹配限定在**原始调用名**上(`can` 作为被调名才作数)。
|
|
316
316
|
*/
|
|
317
|
+
/**
|
|
318
|
+
* §47(2026-09-27)**会话承载型请求**:参数类型名以 `Session` 结尾
|
|
319
|
+
* (`RequestWithSession` / `AuthenticatedSession` / `HttpSession` / `Session`)。
|
|
320
|
+
*
|
|
321
|
+
* 语义:函数手里握着一个 session 对象 ⇒ 这次调用**已经落在某个会话上下文里**,
|
|
322
|
+
* 「任何人无需凭证即可访问/修改」这个结论不成立。这是**正向**证据且来自 AST 签名,
|
|
323
|
+
* 不像「calls 里没有 xxx」那样会被提取失败伪装过去(fp-pool 入边为 0 的函数占
|
|
324
|
+
* 36%~42%,依赖 calls 的机制在那些函数上会静默失效)。
|
|
325
|
+
*
|
|
326
|
+
* ⚠ 只喂给「未认证」两条规则,**不**给 Ownership / Resource Ownership 补 ——
|
|
327
|
+
* 有会话 ≠ 操作对象属于自己。R66 的否命题由这对组合承担:
|
|
328
|
+
* 同形状的越权读取 `getOthersData(req: RequestWithSession)` 应当
|
|
329
|
+
* Unauthenticated 熄灭、**Ownership 继续亮**(fixture 见
|
|
330
|
+
* src/protocol-detector-session-param.test.ts 的反向组)。
|
|
331
|
+
*/
|
|
332
|
+
const SESSION_BEARING_TYPE_RE = /(?:^|[A-Za-z])Session$/i;
|
|
317
333
|
const SAFEGUARD_RULES = [
|
|
318
334
|
// ── Password Hashing ──
|
|
319
335
|
{
|
|
@@ -407,6 +423,8 @@ const SAFEGUARD_RULES = [
|
|
|
407
423
|
category: "authorization",
|
|
408
424
|
languages: ["typescript", "javascript", "python"],
|
|
409
425
|
paramGated: true,
|
|
426
|
+
// §47:签名带 session ⇒ 已落在会话上下文中(详见 SESSION_BEARING_TYPE_RE 注释)
|
|
427
|
+
paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
|
|
410
428
|
trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
|
|
411
429
|
safeguards: [
|
|
412
430
|
{ pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__)\b/i, label: "auth_check" },
|
|
@@ -439,6 +457,8 @@ const SAFEGUARD_RULES = [
|
|
|
439
457
|
category: "authorization",
|
|
440
458
|
languages: ["typescript", "javascript", "python"],
|
|
441
459
|
paramGated: true,
|
|
460
|
+
// §47:同上 —— 只压制「未认证」,不压制「越权」
|
|
461
|
+
paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
|
|
442
462
|
// Note: "post" deliberately excluded — it collides with the Post entity name
|
|
443
463
|
// (listPosts/getPost/deletePost fire via identifier-parsed words).
|
|
444
464
|
trigger: /\b(add|create|update|set|publish|insert|submit)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Add|Create|Update|Set|Publish|Insert|Submit)\b/i,
|
|
@@ -1485,6 +1505,12 @@ function detectSafeguardViolations(calls, enclosingFuncName, language, params, e
|
|
|
1485
1505
|
if (!hasIdentity && !exposed)
|
|
1486
1506
|
continue;
|
|
1487
1507
|
}
|
|
1508
|
+
// §47(2026-09-27)签名级会话证据:命中即视为「已认证」,与 calls 类 safeguard 并列。
|
|
1509
|
+
// 放在 `guards.find` 之前,语义上等价于命中了一个 safeguard。
|
|
1510
|
+
if (rule.paramTypeSafeguards && paramTypes) {
|
|
1511
|
+
if (paramTypes.some((t) => t && rule.paramTypeSafeguards.some((re) => re.test(t))))
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1488
1514
|
// Check if at least one safeguard matches
|
|
1489
1515
|
const guards = (params && rule.strictSafeguards) ? rule.strictSafeguards : rule.safeguards;
|
|
1490
1516
|
const matchedSafeguard = guards.find(s => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.54",
|
|
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/",
|