progmune-runtime 3.7.52 → 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 +75 -0
- package/dist/extract-ir.js +21 -2
- package/dist/protocol-detector-acronym.test.js +118 -0
- package/dist/protocol-detector-authz-drift.test.js +65 -0
- package/dist/protocol-detector-authz-machinery.test.js +83 -0
- package/dist/protocol-detector-authz-selfpred.test.js +105 -0
- package/dist/protocol-detector-authz.test.js +78 -0
- package/dist/protocol-detector-credential-issuance.test.js +64 -0
- package/dist/protocol-detector-owner-self.test.js +57 -0
- package/dist/protocol-detector-paramgate.test.js +105 -0
- package/dist/protocol-detector-session-param.test.js +93 -0
- package/dist/protocol-detector.js +319 -12
- package/dist/sdk.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
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
|
+
|
|
40
|
+
## [3.7.53] — 2026-09-26
|
|
41
|
+
|
|
42
|
+
### 授权族误报压降收口 + 三处可见性基建(§35-§46 合并发布)
|
|
43
|
+
|
|
44
|
+
**可见性基建(§35-§37,只读探针,不进产品路径)**:跨函数调用链视图原型;采样单位从
|
|
45
|
+
「文件」换成「入口的调用闭包」;入口根可达性视图(「谁会走到我」)。三者共同解决
|
|
46
|
+
**切片看不出跨函数证据**的问题——后续每一轮压降都靠它们回答「这一刀在哪个语料上被触发」。
|
|
47
|
+
|
|
48
|
+
**两处判据进主干(§38、§39)**:
|
|
49
|
+
- 对象级授权判据从「动词白名单」换成「组合式谓语」,不再靠 `get/list/create` 这类词根猜语义
|
|
50
|
+
- D 类自指排除:把**鉴权机制自身**也识别为 auth function(此前机制代码被当成未鉴权的业务代码)
|
|
51
|
+
|
|
52
|
+
**授权族压降(§40-§46)**:
|
|
53
|
+
- §42 补 `webshape_G`(17 条授权形态)——**此前盲测在授权命名轴上根本没有差分能力**,
|
|
54
|
+
改任何判据都看不出效果;补形后项目 117→118、Authorization 族 938→947
|
|
55
|
+
- §44 `paramGated`:**标量参数不是调用者身份**。函数参数名出现 `userId/ownerId` 但类型是
|
|
56
|
+
裸 string/number 的,不再当鉴权证据(`classifyParamType` 三分类);真实池 337→333、
|
|
57
|
+
盲测 2887→2618,269 条损失全为 dispatcher 重复投影
|
|
58
|
+
- §45 修同族 safeguard 词表漂移(Unauthenticated Access 缺 `getSession/validateSession/
|
|
59
|
+
verifyToken`)——漂移的表现是**同样形状的一条报、一条不报**;盲测 2618→2610
|
|
60
|
+
- §46 ① `credential_issuance`:`create_*_token` 语义是**签发**不是**校验**,从 `auth_check`
|
|
61
|
+
移出单列(行为不变,消除误改诱因);② `owner_self_assigned`:只认**绑定形态**
|
|
62
|
+
`{ownerId: user.id}`,不认实参形态 `f(x, user.id)`(真实池 3/3 FP vs 合成 1/1 TP,
|
|
63
|
+
真值相反 ⇒ R71),同刻比对压掉 197 条全 FP、真漏洞零伤
|
|
64
|
+
|
|
65
|
+
**三条被数据否决的(不落地)**:§43「容器后缀=Service」(造不出反例,R66)、
|
|
66
|
+
§45「triggerOwnNameOnly」(R66 二次命中)、§46③「safeguard 只看调用不看是否检查返回结果」
|
|
67
|
+
(新增 26 条全是已知 dispatcher 误报,收益 0,R72)。
|
|
68
|
+
|
|
69
|
+
**方法学累计**:R66-R73。其中两条是**基线陷阱**——R67(冻结母本不是同刻基线,误报压制数
|
|
70
|
+
48→真实 4)、R73(上一次的 results 也不是同刻基线,误报压制数 12→真实 197,低估 16 倍)。
|
|
71
|
+
正确口径只有一个:**摘掉这一刀、其余不动、重跑**。次要:R68(dist 不是 src 的同步副本,
|
|
72
|
+
陈旧是静默的)、R69(同族词表要定期做集合 diff)、R70(label 必须与语义一致)。
|
|
73
|
+
|
|
74
|
+
**门**:tsc 0 错 / webshape 66-0 / taintpath 160-0 / fr-007 pre5·post0 + fr-016 pre7·post0 /
|
|
75
|
+
定向 9 文件 329 条全绿 / 反向验证:② 摘刀 197 条全部回来、① 摘刀 fixture 转红、
|
|
76
|
+
REVERSE-VERIFY 残留 0 / 真实切片 LOST 0·ADDED 0
|
|
77
|
+
|
|
3
78
|
## [3.7.52] — 2026-09-23
|
|
4
79
|
|
|
5
80
|
### 语料补形 + 两处基础设施修复 + E4/E5 两个确定性通道(§26-§31 合并发布)
|
package/dist/extract-ir.js
CHANGED
|
@@ -379,6 +379,19 @@ function extractDirectCalls(func, preText) {
|
|
|
379
379
|
if (/ownerId\s*[!=]==?|authorId\s*[!=]==?|createdBy\s*[!=]==?|\.owner\s*[!=]==?|userId\s*[!=]==?/.test(text)) {
|
|
380
380
|
calls.push("__progmune_ownership_checked__");
|
|
381
381
|
}
|
|
382
|
+
// §46 ②:新建资源的属主被**赋值为当前主体**(`{ userId: user.id }` / `ownerId: currentUser.id`)。
|
|
383
|
+
// 这类「创建」函数的被操作对象就是主体自己,不存在「改别人的东西」这回事 ⇒
|
|
384
|
+
// Ownership Check 不该报(合成语料上 10/10 是 FP:createTransfer / createRefund /
|
|
385
|
+
// uploadFile / placeOrder…)。调用名接口看不到对象字面量里的赋值,只能靠标记。
|
|
386
|
+
//
|
|
387
|
+
// ⚠ 只认 `<属主字段>: <主体>.id` 这一种形态,**不认** `<调用>(..., user.id, ...)`:
|
|
388
|
+
// 后者是「主体标识作实参」,实测同形状存在相反真值 ——
|
|
389
|
+
// `deletePAT(id, user.uid)`(已按属主过滤,FP)vs `deleteWidget(id)` 里
|
|
390
|
+
// `canSendEmail(user.id)`(user.id 与 deleteWidget 无关,真 TP)⇒ 按 R66 不得落地。
|
|
391
|
+
// 形态:`<属主字段>: <主体变量>.<标识属性>` (对象字面量内的赋值,不是查询比较)
|
|
392
|
+
if (/\b(?:userId|ownerId|authorId|createdBy|user_id|owner_id|author_id|created_by)\s*:\s*(?:user|currentUser|authUser|me|self|actor|principal|adminUser|(?:request|req|session|ctx|context)\.user)\s*\.\s*(?:id|uid|_id|userId|email|username)\b/i.test(text)) {
|
|
393
|
+
calls.push("__progmune_owner_self_assigned__");
|
|
394
|
+
}
|
|
382
395
|
return [...new Set(calls)];
|
|
383
396
|
}
|
|
384
397
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -403,8 +416,14 @@ const PATH_TRAVERSAL_MARKER = "__progmune_path_traversal__";
|
|
|
403
416
|
// __progmune_auth_machinery__,TS 侧此前从未产出 ⇒ 规则侧零改动。
|
|
404
417
|
//
|
|
405
418
|
// 边界(写死在这里,别想当然):只有**接受标记的规则**会受益。
|
|
406
|
-
//
|
|
407
|
-
//
|
|
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 摸底时差点据此重开一条已修好的通道)。
|
|
408
427
|
// ═══════════════════════════════════════════════════════════════
|
|
409
428
|
const AUTH_MACHINERY_MARKER = "__progmune_auth_machinery__";
|
|
410
429
|
/** 显式免鉴权装饰器 —— 出现即不注入。把公开端点判成「有鉴权」是最危险的假阴性。 */
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* §41 E1(2026-09-25):token 内嵌 acronym 的**局部**补认 —— 正/反向 fixture
|
|
5
|
+
*
|
|
6
|
+
* 背景:C5 原案是「改主干 identifierParse 让 JWTPayload 拆出 JWT」。
|
|
7
|
+
* `split-probe.ts` 实测否决了它(详见 src 里 ACRONYM_PREFIX 的注释):
|
|
8
|
+
* 803 个去重标识符里只有 16 个受影响、新增 22 词中仅 2 个(JWT/OIDC)是机制类,
|
|
9
|
+
* 其余是 `OAuthToken→Auth`、`createATeam→A,Team` 这类噪声;
|
|
10
|
+
* 而 identifierParse 还供给 triggerParsedWords ⇒ 动它等于改全局触发面。
|
|
11
|
+
* ⇒ 最终落地的是**只在 isAuthMachineryName 的对象侧**补认 acronym。
|
|
12
|
+
*
|
|
13
|
+
* 为什么必须有这里的 fixture(同 §40,R56):改动方向是「压掉告警」,
|
|
14
|
+
* 而 generated/ 合成语料里**没有** `verifyJWTPayload` 这类形态 ⇒
|
|
15
|
+
* 盲测零漂移很可能是零触发。**这里是唯一能证明这段判据活着的证据。**
|
|
16
|
+
*/
|
|
17
|
+
const vitest_1 = require("vitest");
|
|
18
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
19
|
+
/** 只关心 authorization 类目(本轮只影响这一类目) */
|
|
20
|
+
const authz = (fnName, calls = ["deleteSpace", "findOne"]) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
|
|
21
|
+
(0, vitest_1.describe)("§41 E1 正向:acronym 卡在函数名的机制对象里(应该被认成鉴权机制)", () => {
|
|
22
|
+
(0, vitest_1.it)("verifyJWTPayload —— §40 剩余池待救的那条(verdaccio packages/auth/src/utils.ts)", () => {
|
|
23
|
+
(0, vitest_1.expect)(authz("verifyJWTPayload")).toHaveLength(0);
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)("verifyJWTPayload 带触发 trigger 也一样压", () => {
|
|
26
|
+
(0, vitest_1.expect)(authz("verifyJWTPayload", ["getTarball", "readPackage"])).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
(0, vitest_1.it)("JWTRemote 类托管的 jwt 机制方法", () => {
|
|
29
|
+
(0, vitest_1.expect)(authz("JwtRemote.getJWTRemote")).toHaveLength(0);
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)("类限定形式 AvailabilityService.verifyJWTPayload", () => {
|
|
32
|
+
(0, vitest_1.expect)(authz("AvailabilityService.verifyJWTPayload")).toHaveLength(0);
|
|
33
|
+
});
|
|
34
|
+
(0, vitest_1.it)("createJWTToken —— 唯有 E1 才救得回来的一条(反向验证揪出来的错误假设)", () => {
|
|
35
|
+
// ⚠ 这条曾经被我标成「BASE 就命中」,是**错的**:
|
|
36
|
+
// identifierParse("createJWTToken") → ["create","JWTToken"]
|
|
37
|
+
// ⇒ 对象侧只拿到 "jwttoken",既 ≠ jwt 也 ≠ token ⇒ BASE **不命中**。
|
|
38
|
+
// 反向验证(把 machineryWords 退回 identifierParse)让它转红才发现。
|
|
39
|
+
// 教训同 R59:凭印象断言既有行为,与口径漂移是同一类错误。
|
|
40
|
+
(0, vitest_1.expect)(authz("createJWTToken")).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.it)("⚠ verifyOIDCToken 不收 —— oidc 只在**容器侧**名词表,**不在方法对象词表**", () => {
|
|
43
|
+
// §39.8 的收窄是有意的:对象侧一旦放宽到 oauth/oidc/saml,
|
|
44
|
+
// `ApiTokensController.deleteToken` 这类凭据 CRUD 会被误压(V-D 教训)。
|
|
45
|
+
// 剩余池里没有待救的 verifyOIDCToken 形态 ⇒ 按 R50 不为此扩词表。
|
|
46
|
+
(0, vitest_1.expect)(authz("verifyOIDCToken").length).toBeGreaterThan(0);
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.it)("verifySSLSignature 不在机制对象词表 ⇒ 仍报(词表不含 ssl,别幻想)", () => {
|
|
49
|
+
(0, vitest_1.expect)(authz("verifySSLSignature").length).toBeGreaterThan(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
/**
|
|
53
|
+
* `tokenAcronyms` 的直接单元测试 —— 端到端 fixture 覆盖不到这两条约束,
|
|
54
|
+
* 因为**对象侧 acronym 与 BASE 的重叠度极高**(凡 acronym 等于 jwt/token/…
|
|
55
|
+
* 的,BASE 下独立 token 本来也就命中了),端到端分不出「约束生效」与
|
|
56
|
+
* 「恰好 BASE 也命中」。剩下来真正会被 `${2,}` 拦住的,只有单字母情形。
|
|
57
|
+
*/
|
|
58
|
+
(0, vitest_1.describe)("§41 tokenAcronyms 表驱动:两条约束(≥2 字母 / 贪婪回溯)", () => {
|
|
59
|
+
const cases = [
|
|
60
|
+
["JWTPayload", ["JWT"]],
|
|
61
|
+
["OIDCStrategy", ["OIDC"]], // ← 贪婪:非贪婪会停在 "OID",救不了这条
|
|
62
|
+
["HTTPErr", ["HTTP"]],
|
|
63
|
+
["CLIError", ["CLI"]],
|
|
64
|
+
["JWTToken", ["JWT"]],
|
|
65
|
+
// 以下必须为空:单字母 / 纯 PascalCase / 已有独立词
|
|
66
|
+
["OAuth", []], // ← 放宽到 {1,} 就会产出 "O"
|
|
67
|
+
["ATeam", []],
|
|
68
|
+
["YElement", []],
|
|
69
|
+
["Token", []],
|
|
70
|
+
["Payload", []],
|
|
71
|
+
["payload", []],
|
|
72
|
+
["JWTPayloadService", ["JWT"]], // 边界:最后一个大写归后词 ⇒ 不会吃成 "JWTP"
|
|
73
|
+
];
|
|
74
|
+
for (const [input, want] of cases) {
|
|
75
|
+
(0, vitest_1.it)(`${input} → ${JSON.stringify(want)}`, () => {
|
|
76
|
+
(0, vitest_1.expect)((0, protocol_detector_1.tokenAcronyms)(input)).toEqual(want);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.describe)("§41 E1 反向:不得把非机制名字误认成机制(R57 宽变体约束)", () => {
|
|
81
|
+
(0, vitest_1.it)("⚠ validateOAuthToken —— BASE 就会命中(validate × Token),与 acronym 无关", () => {
|
|
82
|
+
// 存在意义:端到端根本测不出「OAuth 被拆出 Auth」—— 因为这条 BASE 下也压。
|
|
83
|
+
// 若有人把 `{2,}` 放宽成 `{1,}`,`OAuth` → "O",而 "O" 不在任何词表里
|
|
84
|
+
// ⇒ 端到端行为完全不变,**回归无从发现**。单字母噪声只有在表驱动层才可见。
|
|
85
|
+
(0, vitest_1.expect)(authz("validateOAuthToken")).toHaveLength(0);
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)("⚠ createATeam 不得拆出 Team 之外的机制词", () => {
|
|
88
|
+
(0, vitest_1.expect)(authz("AdminService.createATeam").length).toBeGreaterThan(0);
|
|
89
|
+
});
|
|
90
|
+
(0, vitest_1.it)("⚠ throwHTTPErr 不得因 HTTP 被当成机制", () => {
|
|
91
|
+
(0, vitest_1.expect)(authz("throwHTTPErr").length).toBeGreaterThan(0);
|
|
92
|
+
});
|
|
93
|
+
(0, vitest_1.it)("⚠ createCLIErrorResponse 不得因 CLI/Error 被当成机制", () => {
|
|
94
|
+
(0, vitest_1.expect)(authz("createCLIErrorResponse").length).toBeGreaterThan(0);
|
|
95
|
+
});
|
|
96
|
+
(0, vitest_1.it)("⚠ 纯 PascalCase 词不得被再切一遍(Token → T/oken 之类的臆想不生效)", () => {
|
|
97
|
+
(0, vitest_1.expect)(authz("deleteExpiredToken").length).toBeGreaterThan(0);
|
|
98
|
+
});
|
|
99
|
+
(0, vitest_1.it)("⚠ 动词缺失 ⇒ acronym 对象不算(只有对象没有机制动词)", () => {
|
|
100
|
+
// mw 必须同时含 VERB × OBJECT;JWTPayload 单独不是机制
|
|
101
|
+
(0, vitest_1.expect)(authz("logJWTPayload").length).toBeGreaterThan(0);
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* 最要紧的一条反向:**凭据增删改**即使名字里含 JWT 也不能压 —— 与 §39 V-D、
|
|
105
|
+
* §40 W4 同源教训。这条守的是「约束在于组合,不在于 acronym 本身」。
|
|
106
|
+
*/
|
|
107
|
+
(0, vitest_1.it)("⚠️ createJWTToken 是依赖 E1 的哨兵 ⇒ 反向验证时必须转红", () => {
|
|
108
|
+
// §41 反向验证实测(把 machineryWords 退回 identifierParse):
|
|
109
|
+
// 全套 76 条里 **6 条**转红,其余 70 条(§38/§39/§40)纹丝不动
|
|
110
|
+
// ⇒ E1 的影响面被严格锁在「机制对象侧 acronym」这一格,没有横向污染。
|
|
111
|
+
// 这条 earlier 被错写成「GENERATE 类动词…既有口径一致」—— 与 R59 同源的
|
|
112
|
+
// 「凭印象断言既有行为」,已据反向验证的实测更正。
|
|
113
|
+
(0, vitest_1.expect)(authz("createJWTToken")).toHaveLength(0);
|
|
114
|
+
});
|
|
115
|
+
(0, vitest_1.it)("⚠ 与 §40 C4 的分工:容器=Auth 且方法受保护动词开头 ⇒ 不压", () => {
|
|
116
|
+
(0, vitest_1.expect)(authz("Auth.changeJWTToken").length).toBeGreaterThan(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* §45:Authorization (Unauthenticated Access) 的 auth_check 词表漂移修复。
|
|
5
|
+
*
|
|
6
|
+
* 病灶:同一族的 Unauthenticated Mutation 认 `getSession` / `validateSession` / `verifyToken`,
|
|
7
|
+
* Unauthenticated Access 只认 `getSessionUser`,**不认裸 `getSession`**。
|
|
8
|
+
* 于是「先取会话再操作」的读类函数被判成「未认证即可读」—— 真实案例 filestorage 的
|
|
9
|
+
* uploadFile / deleteFile / createFolder / createShareLink 四条(体里都有
|
|
10
|
+
* `const user = getSession(token); if (!user) return null;`)。
|
|
11
|
+
*
|
|
12
|
+
* 修复:把三个词补进 Access 规则的 auth_check(与 Mutation 对齐)。
|
|
13
|
+
* 不做:三个 mint 类词(create_access_token / create_jwt_token / create_refresh_token)——
|
|
14
|
+
* 「签发令牌」不是「校验调用者」,语义不同,不在本轮(见设计文档 §45.6)。
|
|
15
|
+
*/
|
|
16
|
+
const vitest_1 = require("vitest");
|
|
17
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
18
|
+
const ACC = "Authorization (Unauthenticated Access)";
|
|
19
|
+
const MUT = "Authorization (Unauthenticated Mutation)";
|
|
20
|
+
/** 单 LANGUAGE=typescript;paramGate 用 user:User 放行(类型是非标量领域类型) */
|
|
21
|
+
function dsv(calls, name) {
|
|
22
|
+
return (0, protocol_detector_1.detectSafeguardViolations)(calls, name, "typescript", ["user"], false, ["User"]);
|
|
23
|
+
}
|
|
24
|
+
const has = (calls, name, rule) => dsv(calls, name).some((v) => v.rule === rule);
|
|
25
|
+
(0, vitest_1.describe)("§45 getSession 漂移:读类函数已取会话 ⇒ 不再报 Unauthenticated Access", () => {
|
|
26
|
+
(0, vitest_1.it)("getSession 应被认作认证检查(filestorage uploadFile 原型)", () => {
|
|
27
|
+
(0, vitest_1.expect)(has(["getSession", "join", "writeFileSync"], "uploadFile", ACC)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)("validateSession 应被认作认证检查", () => {
|
|
30
|
+
(0, vitest_1.expect)(has(["validateSession", "find"], "listDocuments", ACC)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("verifyToken 应被认作认证检查", () => {
|
|
33
|
+
(0, vitest_1.expect)(has(["verifyToken", "find"], "getDocument", ACC)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)("类限定调用形态 this.auth.getSession() 也成立(identifierParse 拆词后命中)", () => {
|
|
36
|
+
(0, vitest_1.expect)(has(["this", "auth", "getSession", "find"], "deleteFile", ACC)).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.describe)("§45 反向对照:真正的未认证读取必须仍然报", () => {
|
|
40
|
+
(0, vitest_1.it)("getDocuments 无任何认证调用 ⇒ 仍报", () => {
|
|
41
|
+
(0, vitest_1.expect)(has(["find", "filter"], "getDocuments", ACC)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)("listReports 无任何认证调用 ⇒ 仍报", () => {
|
|
44
|
+
(0, vitest_1.expect)(has(["filter", "map"], "listReports", ACC)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.it)("downloadExport 无任何认证调用 ⇒ 仍报", () => {
|
|
47
|
+
(0, vitest_1.expect)(has(["readFileSync", "pipe"], "downloadExport", ACC)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.it)("getSessionUser 之外的无关 get* 不构成认证证据(getConfig 类)", () => {
|
|
50
|
+
// getConfig 走 excludePatterns;这里用 getUniqueId 确保普通取值不算检查
|
|
51
|
+
(0, vitest_1.expect)(has(["getUniqueId", "find"], "listAuditLogs", ACC)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.describe)("§45 与 Mutation 规则的对称性:同一形态两条规则都不报", () => {
|
|
55
|
+
const calls = ["getSession", "find"];
|
|
56
|
+
(0, vitest_1.it)("读侧", () => (0, vitest_1.expect)(has(calls, "listInvoices", ACC)).toBe(false));
|
|
57
|
+
(0, vitest_1.it)("写侧", () => (0, vitest_1.expect)(has(["getSession", "push"], "createInvoice", MUT)).toBe(false));
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.describe)("§45 已知未覆盖范围(登记为债,不是 bug)", () => {
|
|
60
|
+
(0, vitest_1.it)("⚠ 只看「是否调用」getSession,不看「是否检查其结果」—— 这是既有宽松", () => {
|
|
61
|
+
// `const u = getSession(t); return data;` (不检查 u)仍会被判为已认证。
|
|
62
|
+
// 单函数视角拿不到数据流,本轮不修;见设计文档 §45.6。
|
|
63
|
+
(0, vitest_1.expect)(has(["getSession", "return"], "getEverything", ACC)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* §39(2026-09-25)D 类自指排除 —— 正/反向 fixture
|
|
5
|
+
*
|
|
6
|
+
* 为什么必须有这个文件:与 §38 同因。本轮改的是「哪些函数算鉴权机制自身」,
|
|
7
|
+
* 盲测语料(blind-benchmark/generated/ 合成项目)里没有 JwtAuthGuard /
|
|
8
|
+
* AbilityFactory 这类形态 ⇒ 盲测会零漂移,但同时**一条都没触发**(R56:
|
|
9
|
+
* 零漂移 ≠ 验证通过)。没有这里的 fixture,这段判据就是零覆盖进主干。
|
|
10
|
+
*
|
|
11
|
+
* ⚠ 反向 fixture 比正向更要紧:这条判据的作用是**压掉**告警,
|
|
12
|
+
* 压错方向就是真漏报且不可见(R49 的镜像)。所以每一条「不得压掉」的
|
|
13
|
+
* 形态都必须钉死,尤其是**对凭据做增删改**的端点(那是最需要鉴权的地方)。
|
|
14
|
+
*
|
|
15
|
+
* 每条都对应真值集/真实扫描里人工核对过的代码:
|
|
16
|
+
* JwtAuthGuard.handleRequest ← docmost src/common/guards/jwt-auth.guard.ts
|
|
17
|
+
* WorkspaceAbilityFactory.createForUser ← docmost src/core/casl/abilities/
|
|
18
|
+
* ApiTokensController.deleteToken ← hedgedoc(凭据 CRUD,不得压)
|
|
19
|
+
* AccessTokenService.updateLastUsedForPAT ← hoppscotch(凭据 CRUD,不得压)
|
|
20
|
+
*/
|
|
21
|
+
const vitest_1 = require("vitest");
|
|
22
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
23
|
+
/** 只关心 authorization 类目(本轮只影响这一类目) */
|
|
24
|
+
const authz = (fnName) => (0, protocol_detector_1.detectSafeguardViolations)(["deleteSpace", "findOne"], fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
|
|
25
|
+
(0, vitest_1.describe)("§39 自指排除 —— 正对照(鉴权机制自身 ⇒ 压掉)", () => {
|
|
26
|
+
(0, vitest_1.it)("JwtAuthGuard 守卫本体", () => {
|
|
27
|
+
(0, vitest_1.expect)(authz("JwtAuthGuard.handleRequest")).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)("JwtAuthGuard 里写 cookie", () => {
|
|
30
|
+
(0, vitest_1.expect)(authz("JwtAuthGuard.setJoinedWorkspacesCookie")).toHaveLength(0);
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("Auth 控制器置 cookie(登录流程的一部分)", () => {
|
|
33
|
+
(0, vitest_1.expect)(authz("AuthController.setAuthCookie")).toHaveLength(0);
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)("Auth 服务签发协作 token", () => {
|
|
36
|
+
(0, vitest_1.expect)(authz("AuthService.getCollabToken")).toHaveLength(0);
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.it)("Token 服务校验 jwt(纯原语)", () => {
|
|
39
|
+
(0, vitest_1.expect)(authz("TokenService.verifyJwt")).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
(0, vitest_1.it)("CASL 授权引擎本体", () => {
|
|
42
|
+
(0, vitest_1.expect)(authz("WorkspaceAbilityFactory.createForUser")).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)("签发 JWT", () => {
|
|
45
|
+
(0, vitest_1.expect)(authz("UserService.generateJWT")).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)("从请求里取 token(裸函数名,无类限定)", () => {
|
|
48
|
+
(0, vitest_1.expect)(authz("getApiToken")).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.describe)("§39 自指排除 —— 反对照(不是机制 ⇒ 不得压掉)", () => {
|
|
52
|
+
(0, vitest_1.it)("业务函数:删收藏(「user 作为实参」的 B 类,不是机制)", () => {
|
|
53
|
+
(0, vitest_1.expect)(authz("FavoriteController.removeFavorite").length).toBeGreaterThan(0);
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)("业务函数:删附件图标", () => {
|
|
56
|
+
(0, vitest_1.expect)(authz("AttachmentController.removeIcon").length).toBeGreaterThan(0);
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)("业务函数:删空间", () => {
|
|
59
|
+
(0, vitest_1.expect)(authz("SpaceController.deleteSpace").length).toBeGreaterThan(0);
|
|
60
|
+
});
|
|
61
|
+
(0, vitest_1.it)("⚠ 凭据 CRUD:删 token —— 最需要鉴权,绝不能压", () => {
|
|
62
|
+
(0, vitest_1.expect)(authz("ApiTokensController.deleteToken").length).toBeGreaterThan(0);
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)("⚠ 凭据 CRUD:删 PAT —— 绝不能压", () => {
|
|
65
|
+
(0, vitest_1.expect)(authz("AccessTokenController.deletePAT").length).toBeGreaterThan(0);
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)("⚠ 凭据 CRUD:改 PAT 使用时间 —— 绝不能压", () => {
|
|
68
|
+
(0, vitest_1.expect)(authz("AccessTokenService.updateLastUsedForPAT").length).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
(0, vitest_1.it)("限流守卫不是鉴权机制(ThrottlerGuard)", () => {
|
|
71
|
+
(0, vitest_1.expect)(authz("ThrottlerGuard.handleRequest").length).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("通用设计模式后缀不是鉴权机制(CacheStrategy)", () => {
|
|
74
|
+
// 只看 Strategy 后缀会误吃 —— 必须配凭据名词约束
|
|
75
|
+
(0, vitest_1.expect)(authz("CacheStrategy.get").length).toBeGreaterThan(0);
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)("受保护操作:改密码不属于机制", () => {
|
|
78
|
+
(0, vitest_1.expect)(authz("AuthService.changePassword").length).toBeGreaterThan(0);
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.it)("待定用户数据删除不属于机制", () => {
|
|
81
|
+
(0, vitest_1.expect)(authz("AuthController.deletePendingUserData").length).toBeGreaterThan(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* §40(2026-09-25)剩余池三处判据缺口 —— 正/反向 fixture
|
|
5
|
+
*
|
|
6
|
+
* 与 §38/§39 同因:这三处都是**压掉告警**方向的改动,盲测语料(generated/ 合成项目)
|
|
7
|
+
* 里没有 `Auth.allow_*` / 裸 `can('publish')` / `canRemove` 这类形态 ⇒ 盲测即便
|
|
8
|
+
* 零漂移也可能**一条都没触发**(R56:零漂移 ≠ 验证通过)。没有这里的 fixture,
|
|
9
|
+
* 这三段判据就是零覆盖进主干。
|
|
10
|
+
*
|
|
11
|
+
* 三处缺口的来源(都是人工核剩余池 35 条时逐条读源码读出来的):
|
|
12
|
+
* C1 裸 can — verdaccio `publish()`:`const can = allow(auth, …)`,
|
|
13
|
+
* 路由上 `can('publish')`。calls 里是**裸** can,
|
|
14
|
+
* §38 的谓语要求 `can<动作词>` ⇒ 落空。
|
|
15
|
+
* C2 自身谓语名 — verdaccio `canRemove` 报 "Ownership Check":它**就是**那个检查。
|
|
16
|
+
* §39 的自指排除只认 AUTH_PATTERN + 机制名,不认 `can<X>`。
|
|
17
|
+
* C4 容器=Auth — §39.8 记下的未收项:`Auth.allow_*` / `Auth.setLegacyAuthCacheEntry`。
|
|
18
|
+
*
|
|
19
|
+
* ⚠⚠ 反向 fixture 比正向更要紧(R57):C4 的**宽变体**(容器放宽到 `Auth*`)实测会吃掉
|
|
20
|
+
* `AuthController.deletePendingUserData` / `Auth.changePassword` / `Auth.invalidateToken`
|
|
21
|
+
* —— 对凭据与待处理用户做增删改恰恰是最需要鉴权的地方。所以 C4 必须
|
|
22
|
+
* 「严格等于裸类名」×「方法是授权决策(allow_)或机制名词」两层约束,两条都要。
|
|
23
|
+
*/
|
|
24
|
+
const vitest_1 = require("vitest");
|
|
25
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
26
|
+
/** 只关心 authorization 类目(本轮只影响这一类目) */
|
|
27
|
+
const authz = (fnName, calls = ["deleteSpace", "findOne"]) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /^Authorization/i.test(v.rule));
|
|
28
|
+
(0, vitest_1.describe)("§40 C1 裸 can(CASL ability.can / verdaccio can('publish'))", () => {
|
|
29
|
+
(0, vitest_1.it)("函数体里调用裸 can ⇒ 认为做了授权判定(verdaccio publish 同形)", () => {
|
|
30
|
+
// publish 的 trigger 来自 publishPackage / removePackage 等,这里用等价形态
|
|
31
|
+
(0, vitest_1.expect)(authz("publish", ["can", "put", "publishPackage", "removePackage"])).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)("裸 cannot 同形", () => {
|
|
34
|
+
(0, vitest_1.expect)(authz("publish", ["cannot", "put", "publishPackage"])).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)("限定形式 ability.can(CASL 标准用法)", () => {
|
|
37
|
+
(0, vitest_1.expect)(authz("publish", ["ability.can", "publishPackage"])).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("⚠ cancel 不是 can —— 精确匹配,不得因同形被收", () => {
|
|
40
|
+
(0, vitest_1.expect)(authz("publish", ["cancel", "publishPackage"]).length).toBeGreaterThan(0);
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.it)("⚠ candidate / canary 也不是 —— 精确匹配钉死", () => {
|
|
43
|
+
(0, vitest_1.expect)(authz("publish", ["candidate", "canary", "publishPackage"]).length).toBeGreaterThan(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.describe)("§40 C2 函数名自身就是授权谓语(自指)", () => {
|
|
47
|
+
(0, vitest_1.it)("canRemove 报「缺所有权检查」是自指谬误", () => {
|
|
48
|
+
(0, vitest_1.expect)(authz("canRemove")).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.it)("canPublish 同理", () => {
|
|
51
|
+
(0, vitest_1.expect)(authz("canPublish")).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
(0, vitest_1.it)("cannotDelete 同理(cannot 分支)", () => {
|
|
54
|
+
(0, vitest_1.expect)(authz("cannotDelete")).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("类限定的 Auth.canRemove 同理", () => {
|
|
57
|
+
(0, vitest_1.expect)(authz("StorageViewCommand.canRemove")).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.it)("⚠ canSendEmail 是限流器不是权限判定 —— 不得收(§38.4 ② 的老反例)", () => {
|
|
60
|
+
(0, vitest_1.expect)(authz("canSendEmail").length).toBeGreaterThan(0);
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("⚠ 名字像谓语但动作词不在表内(canRenderPreview)不得收", () => {
|
|
63
|
+
(0, vitest_1.expect)(authz("canRenderPreview").length).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.describe)("§40 C4 容器严格等于鉴权类 × 授权决策/机制方法", () => {
|
|
67
|
+
(0, vitest_1.it)("Auth.allow_publish —— 授权决策本体(verdaccio)", () => {
|
|
68
|
+
(0, vitest_1.expect)(authz("Auth.allow_publish")).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
(0, vitest_1.it)("Auth.allow_access / allow_unpublish / allow_stage 同族", () => {
|
|
71
|
+
(0, vitest_1.expect)(authz("Auth.allow_access")).toHaveLength(0);
|
|
72
|
+
(0, vitest_1.expect)(authz("Auth.allow_unpublish")).toHaveLength(0);
|
|
73
|
+
(0, vitest_1.expect)(authz("Auth.allow_stage")).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
(0, vitest_1.it)("Auth.setLegacyAuthCacheEntry —— 鉴权缓存内部", () => {
|
|
76
|
+
(0, vitest_1.expect)(authz("Auth.setLegacyAuthCacheEntry")).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)("Authorization.allow_access 同形", () => {
|
|
79
|
+
(0, vitest_1.expect)(authz("Authorization.allow_access")).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)("⚠ Auth.changePassword —— 受保护操作,绝不能压", () => {
|
|
82
|
+
(0, vitest_1.expect)(authz("Auth.changePassword").length).toBeGreaterThan(0);
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* ⚠ 这里踩到一个「测不出来」的坑,记录清楚免得后人以为 C4 放行了它:
|
|
86
|
+
* `Auth.invalidateToken` **在 C4 层确实被 PROTECTED_VERB(invalidate)拦下**,
|
|
87
|
+
* 但 §39 的 AUTH_PATTERN 里本来就有 `invalidate`(登出/会话失效入口),
|
|
88
|
+
* 且它按 identifierParse **逐词**匹配 ⇒ "invalidateToken" 拆出 invalidate ⇒ 先命中。
|
|
89
|
+
* 所以整条链路的结果是"压掉",但**不是 C4 压的**。
|
|
90
|
+
* ⇒ 改成直接断言 C4 这一层,避免把别人的行为记到自己账上。
|
|
91
|
+
*/
|
|
92
|
+
(0, vitest_1.it)("⚠ Auth.invalidateToken —— C4 层必须拦下(受保护动词)", () => {
|
|
93
|
+
(0, vitest_1.expect)((0, protocol_detector_1.isAuthDecisionName)("Auth.invalidateToken")).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.it)("⚠ Auth.add_user —— 用户管理操作,绝不能压", () => {
|
|
96
|
+
(0, vitest_1.expect)(authz("Auth.add_user").length).toBeGreaterThan(0);
|
|
97
|
+
});
|
|
98
|
+
(0, vitest_1.it)("⚠ AuthController.deletePendingUserData —— 容器不是裸 Auth,绝不能压(§39 V-D 教训)", () => {
|
|
99
|
+
(0, vitest_1.expect)(authz("AuthController.deletePendingUserData").length).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)("⚠ AuthService.getCollabToken 之外的业务方法不受影响(AuthService.updateToken)", () => {
|
|
102
|
+
// AuthService 不在"严格等于裸类名"集合里 ⇒ 只有 §39 的机制动词×对象能救
|
|
103
|
+
(0, vitest_1.expect)(authz("AuthService.updateToken").length).toBeGreaterThan(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* §38(2026-09-25)组合式授权谓语 safeguard —— 正/反向 fixture
|
|
5
|
+
*
|
|
6
|
+
* 为什么必须有这个文件:3.7.28–3.7.31 的正则死循环溜过 CI,根因就是
|
|
7
|
+
* 「新增的正则路径没有任何 fixture 触发过它」。本轮新增的三条 safeguard 同理:
|
|
8
|
+
* 盲测语料(blind-benchmark/generated/,合成项目)里**根本没有** cannot/canRemove
|
|
9
|
+
* 这类 CASL 形态 ⇒ 盲测 2865 条零漂移,但也**一条都没触发**(§38.8)。
|
|
10
|
+
* 没有这里的 fixture,这三条正则就是**零覆盖**进主干的。
|
|
11
|
+
*
|
|
12
|
+
* 每条正对照都对应真值集里人工核对过的真实代码:
|
|
13
|
+
* cannot ← docmost SpaceController.deleteSpace
|
|
14
|
+
* canRemove ← verdaccio StorageViewCommand.deleteEntry
|
|
15
|
+
* validateCanEdit ← docmost AttachmentController.uploadFile
|
|
16
|
+
* checkMediaDeletePermission ← hedgedoc MediaController.deleteMedia
|
|
17
|
+
*/
|
|
18
|
+
const vitest_1 = require("vitest");
|
|
19
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
20
|
+
/** 只看所有权/资源所有权两条(本轮只改了这两条的 safeguards) */
|
|
21
|
+
const own = (calls, fnName) => (0, protocol_detector_1.detectSafeguardViolations)(calls, fnName, "typescript", ["user", "id"], true).filter((v) => /Ownership/i.test(v.rule));
|
|
22
|
+
(0, vitest_1.describe)("§38 组合式授权谓语 —— 正对照(有授权 ⇒ 抑制)", () => {
|
|
23
|
+
(0, vitest_1.it)("cannot(CASL 否定谓语)应抑制 Ownership Check", () => {
|
|
24
|
+
// docmost: if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Settings)) throw Forbidden
|
|
25
|
+
(0, vitest_1.expect)(own(["createForUser", "cannot", "deleteSpace"], "deleteSpace")).toHaveLength(0);
|
|
26
|
+
});
|
|
27
|
+
(0, vitest_1.it)("canRemove 应抑制", () => {
|
|
28
|
+
// verdaccio: storage.canRemove(...)
|
|
29
|
+
(0, vitest_1.expect)(own(["canRemove", "removeCachedPackage"], "deleteEntry")).toHaveLength(0);
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)("canPublish 应抑制", () => {
|
|
32
|
+
// verdaccio: auth.allow_publish({packageName}, user, cb) 的包了一层
|
|
33
|
+
(0, vitest_1.expect)(own(["canPublish", "remove"], "stage")).toHaveLength(0);
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)("validateCanEdit(带前缀 helper)应抑制", () => {
|
|
36
|
+
// docmost: await this.validateCanEdit(...)
|
|
37
|
+
(0, vitest_1.expect)(own(["validateCanEdit", "uploadFile"], "uploadFile")).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("checkMediaDeletePermission(A″ 校验动词 + Permission)应抑制", () => {
|
|
40
|
+
// hedgedoc: MediaController.deleteMedia
|
|
41
|
+
(0, vitest_1.expect)(own(["checkMediaDeletePermission", "delete"], "deleteMedia")).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)("validateSpaceAccess 应抑制", () => {
|
|
44
|
+
// docmost: FavoriteController.removeFavorite → resolveAndValidate → validateSpaceAccess
|
|
45
|
+
(0, vitest_1.expect)(own(["resolveAndValidate", "validateSpaceAccess", "removeFavorite"], "removeFavorite")).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.describe)("§38 —— 负对照(同形噪声 ⇒ 仍须报违规)", () => {
|
|
49
|
+
(0, vitest_1.it)("裸 delete 无任何授权 ⇒ 仍报", () => {
|
|
50
|
+
(0, vitest_1.expect)(own(["deleteSpace"], "deleteSpace").length).toBeGreaterThan(0);
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.it)("canSendEmail 是限流器不是权限 ⇒ 仍报", () => {
|
|
53
|
+
// 实测抓到:rateLimiter.canSendEmail(userId) 被第一版误收
|
|
54
|
+
(0, vitest_1.expect)(own(["canSendEmail", "update"], "updateThing").length).toBeGreaterThan(0);
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("isRight 是 fp-ts Either 判定 ⇒ 仍报", () => {
|
|
57
|
+
// 实测抓到:hoppscotch 满仓 fp-ts,一次冒出 2 条假转绿
|
|
58
|
+
// ⚠ 函数名必须能触发 Ownership Check 的 trigger(delete/remove/update…系动词)。
|
|
59
|
+
// 用 addUserToTeam 的话 trigger 不命中 ⇒ 恒 0,测不出东西(第一版就写错了)。
|
|
60
|
+
(0, vitest_1.expect)(own(["isRight", "removeMemberFromTeam"], "removeTeamMember").length).toBeGreaterThan(0);
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.it)("deleteByUsersWithoutSpaceAccess 是数据删除不是校验 ⇒ 仍报", () => {
|
|
63
|
+
(0, vitest_1.expect)(own(["deleteByUsersWithoutSpaceAccess"], "removeMember").length).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* ⚠ 本条在 §40 被**推翻**(2026-09-25),保留在此是为了留下决策痕迹:
|
|
67
|
+
* §38 当时"裸 can 太宽(cancel/candidate 同形)⇒ 不收"只是一个**担心**,没有真值证据;
|
|
68
|
+
* §40 人工核剩余池时发现 verdaccio `publish()` 用的是 `const can = allow(auth, …)` +
|
|
69
|
+
* `can('publish')`、CASL 用 `ability.can(Action.Update, subj)` —— **裸 can 是主流写法**,
|
|
70
|
+
* 而"太宽"的担心可以用**精确匹配**解决(cancel/candidate 根本不等于 can)。
|
|
71
|
+
* ⇒ 现在裸 can 收,但两条护栏必须守住(都在 §40 fixture 里钉死):
|
|
72
|
+
* ① 精确匹配 `^can$` / `.can$`(否则 canSendEmail 拆词出 "can" 会被误收)
|
|
73
|
+
* ② `callsOnly: true`(否则 identifierParse 拆出的词 "can" 会误命中)
|
|
74
|
+
*/
|
|
75
|
+
(0, vitest_1.it)("cancel / candidate 不是 can ⇒ 仍报(精确匹配护栏)", () => {
|
|
76
|
+
(0, vitest_1.expect)(own(["cancel", "candidate", "remove"], "removeThing").length).toBeGreaterThan(0);
|
|
77
|
+
});
|
|
78
|
+
});
|