progmune-runtime 3.7.36 → 3.7.41
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 +294 -0
- package/dist/extract-ir-taint-guard.test.js +204 -0
- package/dist/extract-ir-taint-structural.test.js +874 -3
- package/dist/extract-ir.js +528 -37
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,299 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.41] — 2026-09-20
|
|
4
|
+
|
|
5
|
+
### C4e:helper 的现代写法(箭头 / 函数表达式 / 模块常量)
|
|
6
|
+
|
|
7
|
+
C4b 的「纯塑形 helper」名录是用 `sf.getFunctions()` 扫出来的 —— 那只能拿到
|
|
8
|
+
**函数声明**。而真实 TS 工程里 helper 的主力写法是箭头常量:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
export const withExt = (name: string): string => name + ".md"; // ← 此前整片漏
|
|
12
|
+
export const withExtBlock = (name: string): string => { return name + ".md"; };
|
|
13
|
+
export const withExtExpr = function (name: string): string { return name + ".md"; };
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
现在收集阶段并入变量声明上的箭头与函数表达式,判据(有形参 / 有返回值 /
|
|
17
|
+
体内无 sink)原样不变 —— 只是不再漏掉一大半候选。
|
|
18
|
+
|
|
19
|
+
另外两处放宽:
|
|
20
|
+
|
|
21
|
+
- **模块级字面量常量**:`const EXT = ".doc"; f(n) { return n + EXT; }` 以前因自由
|
|
22
|
+
标识符被判据③拦死(这是 D 族登记过的 known-gap)。现在放行【初值是字符串 /
|
|
23
|
+
数字 / 布尔字面量】的模块级常量 —— 出处可确证。初值是**任意表达式**的常量仍拦
|
|
24
|
+
(`const ROOT = path.resolve(process.cwd(), "runs")`),否则 helper 能把别处的
|
|
25
|
+
污点藏在一个常量后面,等于给任意数据流发通行证。
|
|
26
|
+
- **path 模块的局部别名**:白名单里写死 `path` 一个名字不够用,
|
|
27
|
+
`import * as p from "path"` / `import nodePath from "node:path"` /
|
|
28
|
+
`import { join } from "path"` 现在都能认。只放行 path 模块本身 —— fs / os 不放,
|
|
29
|
+
那会被 helper 用来把 IO 藏进 return(判据②只扫 sink 调用,覆盖不到这一类)。
|
|
30
|
+
|
|
31
|
+
### 探针设计的坑(值得记)
|
|
32
|
+
|
|
33
|
+
第一版探针把 helper 调用直接写在 sink 实参里:`fs.writeFileSync(outDir + "/" + withExt(k))`。
|
|
34
|
+
结果**正负对照一次性全 MARK** —— 因为判定有一条兜底规则:sink 实参窗口里出现
|
|
35
|
+
污点名就标。那样测的根本不是 helper 传播。改成隔离写法后才分得出胜负:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const rel = withExt(k); // 污点只能经 helper 的返回值进来
|
|
39
|
+
fs.writeFileSync(outDir + "/" + rel, "x");
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
同一批复现还纠正了两条备忘:`toSlug`(`p.trim().toLowerCase()`)实测早就穿透,
|
|
43
|
+
不是备忘里记的缺口;`reduce` 首参不绑确认按设计生效。
|
|
44
|
+
|
|
45
|
+
### 验收
|
|
46
|
+
|
|
47
|
+
| 门 | 结果 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| taintpath 闸门 | **69/69**(新增 F 族 10 条:6 mark / 1 suppressed / 3 no-taint) |
|
|
50
|
+
| 既有 D 族 known-gap | `emitHelperFixed` 因本轮闭合,已由 known-gap 转 **mark** |
|
|
51
|
+
| 定向用例 | **63 passed**(structural 单文件)/**153 passed**(含 taint-guard、ssrf-loop、project-ir、C/Go/Java 提取组共 7 个文件)—— 新增 C4e 组 9 条:6 正 3 负;另因同一闭合把一条旧负对照转正,并补「非字面量模块常量」锁边界 |
|
|
52
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持** |
|
|
53
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** —— 放宽收集后守卫侧仍无新增误报 |
|
|
54
|
+
| TS 盲测 | 106 项目,按 path_traversal 口径:**LOST 0 / ADDED 16** —— E 族 9(C4d)+ F 族 6(C4e)+ D 族 `emitHelperFixed` 1(本轮闭合),**基线那 104 个项目零漂移** |
|
|
55
|
+
| tsc | 零错误 |
|
|
56
|
+
|
|
57
|
+
### R7 第五次:generated 语料又是零覆盖
|
|
58
|
+
|
|
59
|
+
实测全量 .ts 里 `export const X = (…) =>` **0 处**,A–E 族对本能力照样空过。
|
|
60
|
+
新增 `taintpath_F`(10 条)之后这道门才重新咬得住。
|
|
61
|
+
|
|
62
|
+
### 反向验证做了三刀(不只摘整体)
|
|
63
|
+
|
|
64
|
+
| 摘掉什么 | 结果 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| 箭头 / 函数表达式的候选收集 | F 族 6 条正例**全部**转漏报(模块常量与 path 别名两条也在内 —— 那两个 helper 本身就是箭头写的) |
|
|
67
|
+
| 模块常量 + path 别名(extraIdents 置空) | 只有 `emitModuleConstHelper` / `emitPathAliasHelper` 两条转漏报,其余箭头正例不受影响 |
|
|
68
|
+
| 反过来把模块常量**放宽**到任意初值 | 「非字面量模块常量」那条负对照立刻变红 —— 证明这条边界用例不是空转 |
|
|
69
|
+
|
|
70
|
+
只摘整体只能证明正例依赖新代码,证明不了**边界负对照**真的咬得住,所以补了第三刀。
|
|
71
|
+
|
|
72
|
+
### 顺带查明(不是 bug,记录以免下次重复怀疑)
|
|
73
|
+
|
|
74
|
+
- `helper 体内有 sink` 形态会被标,来自 `methodSinkParamMap` 的跨函数传播:
|
|
75
|
+
helper 的形参确实流进了 sink,标是对的,与 helper 是否被认成纯塑形无关。
|
|
76
|
+
- 名字级传播的精度边界仍在:`discard(k) { return "fixed.md"; }` 丢弃形参,
|
|
77
|
+
结果仍会被判污染。这是「看得见就传播」的名级近似,修它需要形参-实参位置对齐
|
|
78
|
+
与返回值依赖分析 —— 记入下一步。
|
|
79
|
+
|
|
80
|
+
## [3.7.40] — 2026-09-20
|
|
81
|
+
|
|
82
|
+
### C4d:高阶枚举方法的回调形参
|
|
83
|
+
|
|
84
|
+
`Object.keys(doc)` 的枚举绑定此前只认 `for (const k of|in …)`。同一种数据流还有
|
|
85
|
+
一半写在回调里,那半整片漏:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
Object.keys(doc).forEach((k) => { fs.writeFileSync(outDir + "/" + k, "x"); });
|
|
89
|
+
Object.entries(doc).map(([k, v]) => { fs.writeFileSync(outDir + "/" + k, String(v)); });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
回调形参 = 被枚举到的元素,语义与 for-of 完全等价,没有理由区别对待。现在并入
|
|
93
|
+
同一套枚举绑定:接收者支持「根形态」(`Object.keys(doc).forEach`)与「变量形态」
|
|
94
|
+
(`const ks = Object.keys(doc); ks.forEach`,走 C4c 那条不动点)。
|
|
95
|
+
|
|
96
|
+
**绑定规则的边界**(两条都配了负对照钉死):
|
|
97
|
+
|
|
98
|
+
- `[k, v]` 解构两个都是元素,都绑;`k, i` **只取第一个** —— 第二个是索引,
|
|
99
|
+
绑了就是误报。
|
|
100
|
+
- 方法表只收 `forEach / map / flatMap / filter`。不收 `find/some/every`(谓词语义,
|
|
101
|
+
常与白名单校验同现,绑进去反而给守卫侧喂误报),不收 `reduce`(首参是累加器)。
|
|
102
|
+
|
|
103
|
+
**一条纠正**:此前备忘写「`for...in` 也不绑定」,最小复现实测是错的 ——
|
|
104
|
+
`bindFromEnum` 的正则本就是 `(?:of|in)`,for-in 一直是通的。真缺口只有回调这一处。
|
|
105
|
+
备忘里的「下一步」若不先复现就照着做,会改一个已经对的地方。
|
|
106
|
+
|
|
107
|
+
### C4d-b:回调形态的三处遗漏 + 一处真实误报(同日第二轮)
|
|
108
|
+
|
|
109
|
+
放宽后立刻在新语料上验了剩余写法,初版有两件事没做对:
|
|
110
|
+
|
|
111
|
+
| 形态 | 初版 | 现在 |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `forEach(k => …)` 无括号单参 | 漏 | 标记 |
|
|
114
|
+
| `forEach(function (k) { … })` ES5 回调 | 漏 | 标记 |
|
|
115
|
+
| `doc.sections.forEach((s: any) => …)` 成员链 + 类型标注形参 | 漏 | 标记 |
|
|
116
|
+
| `forEach(handleOne)` 回调是**函数引用** | **误标** | 不标记 |
|
|
117
|
+
|
|
118
|
+
误报那条值得单独看:`handleOne` 是别人函数的名字,不是被枚举的元素,但初版正则
|
|
119
|
+
以 `[,)]` 收尾,把它当形参绑进了污点集合 —— 同名的局部变量随即被判成污点。
|
|
120
|
+
修法是把**「回调必须内联」**写进正则前提:箭头分支形参后必须见到 `=>`,ES5 分支
|
|
121
|
+
必须见到 `function` 关键字,函数引用形态自然落空。
|
|
122
|
+
|
|
123
|
+
顺带发现形参字符集漏了 `:`:`(s: any) =>` 这种带类型标注的写法在 TS 工程里是常态,
|
|
124
|
+
一个冒号就让整环不匹配。现放行 `:.|<>`(类型标注、联合类型与泛型形参),
|
|
125
|
+
`=>` 与 `{}` 仍不在集合内,函数体不会被吃进来。
|
|
126
|
+
|
|
127
|
+
### 验收
|
|
128
|
+
|
|
129
|
+
| 门 | 结果 |
|
|
130
|
+
|---|---|
|
|
131
|
+
| fr-007 真实语料 | pre 5 / post 0 **维持**(post 侧含 forEach 形态,放宽后仍未新增误报) |
|
|
132
|
+
| fr-016 真实语料 | pre 7 / post 0 **维持** |
|
|
133
|
+
| taintpath 闸门 | **59/59**(新增 E 族 15 条:9 mark / 1 suppressed / 5 no-taint) |
|
|
134
|
+
| 定向用例 | **143 passed / 7 个测试文件**(新增 C4d 组 12 条:7 正 5 负;taint-guard、structural、ssrf-loop、project-ir、C/Go/Java 提取组全绿) |
|
|
135
|
+
| TS 盲测 | 105 项目,按 path_traversal 口径:**LOST 0 / ADDED 9** —— 9 条全部来自新增语料 `taintpath_E`,其余 104 个项目**零漂移**(另有 2 条 resource 类别的 No Input Sanitization 随新语料首次出现,与本类判定无关) |
|
|
136
|
+
| tsc | 零错误 |
|
|
137
|
+
|
|
138
|
+
### R7 第四次:门自身必须有覆盖
|
|
139
|
+
|
|
140
|
+
generated 全量 .ts 里 `.forEach/.map/.flatMap/.filter` 回调形参 **0 处**(A–D 族都没有),
|
|
141
|
+
所以「LOST 0 / ADDED 0」对这项能力仍然是空过。新增 `taintpath_E`(10 条:
|
|
142
|
+
6 mark / 1 suppressed / 3 no-taint)后,该门才重新咬得住。
|
|
143
|
+
|
|
144
|
+
## [3.7.39] — 2026-09-20
|
|
145
|
+
|
|
146
|
+
### C4b:项目自有的「纯塑形」helper —— 污点穿过自建封装
|
|
147
|
+
|
|
148
|
+
fr-016 里 `iterateAsyncApiComponents` / `iterateComponents` 在 3.7.38 之后仍看不到,
|
|
149
|
+
原因是污点断在最后一跳:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
const filename = getFileNamePath(componentDirPath, componentName, ext);
|
|
153
|
+
// getFileNamePath(a, b, c) { return path.join(a, b) + `.${c}`; }
|
|
154
|
+
writeToFileByExtension(componentData, filename);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`componentName` 已被 `Object.keys` 污染,但 `getFileNamePath` 不在 C4 的塑形词表里
|
|
158
|
+
(那是 node:path 家族 + String 原型方法的**固定清单**,覆盖不到每个项目自己的封装)。
|
|
159
|
+
|
|
160
|
+
修法不是放宽词表,而是**按函数体证明它只做塑形**,三条判据全中才传播:
|
|
161
|
+
|
|
162
|
+
1. 有形参、且有带表达式的 `return`(void / 只写文件的函数不算)
|
|
163
|
+
2. 函数体内**没有**文件 sink —— 含 sink 说明它不只是塑形
|
|
164
|
+
3. 每个 return 表达式里:所有调用都在塑形白名单内,且所有自由标识符都是自己的形参
|
|
165
|
+
|
|
166
|
+
判据③里的**字符过滤方法刻意不算证据**(新增方法学规则 **R11**):C4 的内联规则把
|
|
167
|
+
`.replace/.trim/...` 当塑形,但那是「看得见整个实参窗口」时的取舍;helper 形式看不见,
|
|
168
|
+
而 `.replace(/[^a-z0-9]/gi, "")` 恰恰是净化函数的标准写法。helper 侧的证据集必须比
|
|
169
|
+
内联侧更严。有界两轮不动点,允许 helper 调已认定的 helper,不追环。
|
|
170
|
+
|
|
171
|
+
### C4c:迭代已被污染的聚合
|
|
172
|
+
|
|
173
|
+
`const ks = Object.keys(o); for (const k of ks)` —— 被迭代对象已是**被污染的变量**
|
|
174
|
+
而非根表达式,枚举绑定此前只按根匹配,这种「先收集、再迭代」的写法一根都收不到。
|
|
175
|
+
现把枚举绑定并入有界不动点,与塑形传播、单跳赋值一起迭代至收敛(≤3 轮)。
|
|
176
|
+
|
|
177
|
+
### 验收
|
|
178
|
+
|
|
179
|
+
| 门 | 结果 |
|
|
180
|
+
|---|---|
|
|
181
|
+
| fr-016 真实语料 | **pre 5 → 7**(新增的两条正是 C4b 目标,均为真阳性) |
|
|
182
|
+
| fr-007 真实语料 | pre 5 / post 0 维持 |
|
|
183
|
+
| taintpath 闸门 | **44/44**(新增 D 族 10 条) |
|
|
184
|
+
| TS 盲测 | 见下(D 族专为 R7 补,否则这两处空过) |
|
|
185
|
+
| 反向验证 | 回退 v3.7.37 后对应用例失败,负对照全绿 |
|
|
186
|
+
|
|
187
|
+
### R7 的第三次应用:这次也先补了语料
|
|
188
|
+
|
|
189
|
+
C4b / C4c 在含 C 族的既有盲测语料上**仍然空过** —— 实测 generated 全量 .ts 里
|
|
190
|
+
「先收集再迭代」形态 0 处、「自有 path 塑形 helper」0 处。因此本轮把 fr-016 的
|
|
191
|
+
helper 形态搬进盲测做成 `taintpath_D`(10 条),重生成基线后才比漂移。
|
|
192
|
+
|
|
193
|
+
D 族的守卫侧刻意用 `stamp`(不含任何 G-C 后缀)而非 `assertWithinDir`:
|
|
194
|
+
若压制真的发生,依据就只能是被调用方自身的证据(G2 tier-0),不是名字。
|
|
195
|
+
|
|
196
|
+
## [3.7.38] — 2026-09-19
|
|
197
|
+
|
|
198
|
+
### fr-016:补「文档解析产物」根 + sink 形参继承(召回,两侧各缺一环)
|
|
199
|
+
|
|
200
|
+
真实语料 fr-016(Redocly/redocly-cli `split` 命令路径穿越,GHSA-657c-g7qc-r9j2)
|
|
201
|
+
在连续三轮改动之后仍是 **pre 0 / post 0**。逐段拆开才发现断点是**两个独立缺陷**,
|
|
202
|
+
且两侧对称——只补一侧,语料纹丝不动:
|
|
203
|
+
|
|
204
|
+
- **来源侧**:污点根表里没有「外部文档」。文档本体由上游 `parseYaml` 解析好后
|
|
205
|
+
**作为形参**传入(`channels: Record<string, any>`),函数体内无解析调用,唯一
|
|
206
|
+
本地可见入口是 `for (const channelName of Object.keys(channels))` 这次枚举。
|
|
207
|
+
- 新增根 `document_parse`:`JSON.parse` / `YAML.load` / `parseYaml` … 的解析产物
|
|
208
|
+
- 新增根 `runtime_key_enum`:`Object.keys/values/entries` 的枚举产物
|
|
209
|
+
(内部注释标明它是**性质较弱**的一类根:声明的是「名字不是字面量、而是运行时
|
|
210
|
+
数据结构的产物」,与按传输面声明的前两条不同;是否加害交给守卫判定)
|
|
211
|
+
- `collectTaintedNames` 增加 for-of 绑定收集(含 `[k, v]` 解构)
|
|
212
|
+
|
|
213
|
+
- **sink 侧**:落盘经 `writeToFileByExtension → writeYaml → fs.writeFileSync`
|
|
214
|
+
两层自有封装,`methodSinkParamMap` 原本只登记「形参 → 本函数体内 fs sink」
|
|
215
|
+
一跳,整层 wrapper 从未入表。现增加一个有界闭包(≤3 轮):**自己的形参被传进
|
|
216
|
+
已登记函数的 sink 位 ⇒ 继承该 sink 位**。注意这不是 C4b——C4b 是值侧,这是 sink 侧。
|
|
217
|
+
|
|
218
|
+
### 验收
|
|
219
|
+
|
|
220
|
+
| 门 | 结果 |
|
|
221
|
+
|---|---|
|
|
222
|
+
| **fr-016 真实语料** | **pre 5 条 / post 0 条**(此前 0/0)—— 成为继 fr-007 之后**第二个**有判别力的真实语料对 |
|
|
223
|
+
| fr-007 真实语料 | pre 5 / post 0 维持(未压掉真阳性) |
|
|
224
|
+
| taintpath 闸门 | 24/24 |
|
|
225
|
+
| 全组回归 | 87 passed;`tsc` 零错误 |
|
|
226
|
+
| TS 盲测 102 项目 | LOST 0 / ADDED 0(见下) |
|
|
227
|
+
|
|
228
|
+
fr-016 语料同步升级:从 mini 切片(5 个真值文件)换成完整 `packages/cli` 子包,
|
|
229
|
+
快照入库 `blind-benchmark/fr-corpus/fr-016-redocly/{pre,post}`(各 1.1M,受保护资产);
|
|
230
|
+
`check-fr-corpus.ts` 登记 `fr-016: { pre: 5, post: 0 }`,此后一条命令即可复核。
|
|
231
|
+
|
|
232
|
+
### 诚实说明(避免把水印当证据)
|
|
233
|
+
|
|
234
|
+
G-C 的 `Within` 后缀当初是照着 fr-016 的 `assertWithinDir` 加的,所以 post=0
|
|
235
|
+
对 **G-C** 是一种同义反复。真正结实的证据是新增的定向用例:把守卫函数改名为
|
|
236
|
+
不含任何 G-C 后缀的 `stamp`、只保留函数体内的 `resolve + startsWith(base + sep)`,
|
|
237
|
+
依然被压制 ⇒ 压制依据是被调用方自身的证据(G2 tier-0),不是名字。
|
|
238
|
+
|
|
239
|
+
### 新增方法学规则 R10
|
|
240
|
+
|
|
241
|
+
一条污点流要被观测到,**来源侧与 sink 侧必须同时连通**。补一侧时语料往往纹丝
|
|
242
|
+
不动,看起来像修补无效。调试须先写最小复现逐段确认通断——总数是唯一结果变量,
|
|
243
|
+
任何一侧断着都等于 0,没有定位能力。
|
|
244
|
+
|
|
245
|
+
### 已知缺口
|
|
246
|
+
|
|
247
|
+
- C4b:值侧助手(`buildPath(p)`、`getFileNamePath(a,b,c)`)不传播 —— fr-016 的
|
|
248
|
+
`iterateAsyncApiComponents` / `iterateComponents` 仍因此看不到
|
|
249
|
+
- C4c:`const names = Object.keys(o); for (const n of names)` —— 迭代**已被污染
|
|
250
|
+
的聚合**(而非直接枚举根)不传播
|
|
251
|
+
|
|
252
|
+
## [3.7.37] — 2026-09-19
|
|
253
|
+
|
|
254
|
+
### G2:自定义校验函数的调用点抑制(判别力)
|
|
255
|
+
|
|
256
|
+
缺口实证(taintpath_B `dispatchToolGuarded`,measured):
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
assertTemplateName(args.name); // 函数体内是 /^[A-Za-z0-9_-]+$/ —— 真校验
|
|
260
|
+
return loadTemplate(args.name); // 仍被标记(误报)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`assertTemplateName` 名字不含路径语义后缀——`Name` 在修 `ensureDir()` 误判时被
|
|
264
|
+
整体移出了 G-C 后缀表,于是调用点侧认不出来。这是 G1 收紧词表的已知代价。
|
|
265
|
+
|
|
266
|
+
修法**不是**把 `Name` 加回词表(那是按名字猜语义,G-C 已经为这份宽松付过代价:
|
|
267
|
+
fr-007 pre 侧召回归零),而是看**被调用方函数体内到底有没有校验证据**:
|
|
268
|
+
|
|
269
|
+
- `pathGuardFunctionNames` 拆两档:tier-0 `direct`(自身含证据)/ tier-1+ `all`(推断)
|
|
270
|
+
- `collectSanitizedExprs`:被调用方属 tier-0 ⇒ 把调用实参里的取值表达式收进
|
|
271
|
+
「已净化」集合;sink 实参命中污点但已被净化 ⇒ 不算污点
|
|
272
|
+
|
|
273
|
+
### 三处精度取舍(全部比 G1 的函数级 selfGuarded 窄)
|
|
274
|
+
|
|
275
|
+
| 取舍 | 说明 |
|
|
276
|
+
|---|---|
|
|
277
|
+
| 表达式级,非函数级 | G1 是一个校验词汇压掉整个函数体的所有流;G2 只净化被传进守卫调用的那个表达式,同函数体里另一条未校验的流仍标记 |
|
|
278
|
+
| 只认 tier-0 | 推断出来的守卫不用于抑制 —— 升为方法学规则 **R9-no-inferred-suppression**(抑制不可逆,压掉的真阳性不会出现在任何回归统计里) |
|
|
279
|
+
| 前缀 `(?:^\|[^\w$.])` | 净化 `name` 不连坐 `other.name`;但 `args.name` 能命中 `path.join(DIR, args.name)` |
|
|
280
|
+
|
|
281
|
+
### 验收
|
|
282
|
+
|
|
283
|
+
- taintpath 闸门 **24/24**(`dispatchToolGuarded` mark → suppressed;正对照
|
|
284
|
+
`dispatchToolBare` 仍 mark)
|
|
285
|
+
- **fr-007 openhop 真实语料维持 pre 5 / post 0** —— G2 未压掉真阳性
|
|
286
|
+
- TS 盲测(102 项目):**LOST 1 / ADDED 0**,唯一变化就是本条;覆盖力 19 → 18
|
|
287
|
+
- `extract-ir-taint-guard.test.ts` 新增 G2 组 6 条(含正/负对照,R6)
|
|
288
|
+
- 反向验证:回退 v3.7.36 后 G2 两条正例失败,其余 18 条仍绿
|
|
289
|
+
- 新增 `blind-benchmark/check-fr-corpus.ts` —— 把 fr-007 的 pre/post 复测固化成
|
|
290
|
+
一条命令;快照置于 `blind-benchmark/fr-corpus/`(**受保护资产**,不再放 /tmp)
|
|
291
|
+
|
|
292
|
+
### 已知缺口 G2b
|
|
293
|
+
|
|
294
|
+
经项目自有 helper 转手的校验证据不生效(`checkName(n)` 内部再调守卫函数)——
|
|
295
|
+
只认 tier-0 的代价,与 C4b 同族。缺口是可见的(仍有误报),压掉的真阳性不可见。
|
|
296
|
+
|
|
3
297
|
## [3.7.36] — 2026-09-19
|
|
4
298
|
|
|
5
299
|
### C4:污点经路径塑形表达式包装后仍传播
|
|
@@ -356,6 +356,210 @@ export function read(req: any) {
|
|
|
356
356
|
}
|
|
357
357
|
});
|
|
358
358
|
});
|
|
359
|
+
(0, vitest_1.describe)("G2:调用点抑制 —— 按被调用方的实际证据定案(种子=taintpath_B assertTemplateName)", () => {
|
|
360
|
+
// 缺口形态(2026-09-19 measured,taintpath_B dispatchToolGuarded):
|
|
361
|
+
// `assertTemplateName(args.name); return loadTemplate(args.name);`
|
|
362
|
+
// `assertTemplateName` 函数体内是锚定字符集白名单(G-D),是真校验,
|
|
363
|
+
// 但它的名字不含路径语义后缀 —— `Name` 在修 `ensureDir()` 误判时被整体
|
|
364
|
+
// 移出了 G-C 守卫后缀表,于是调用点侧认不出来,仍被标记。
|
|
365
|
+
//
|
|
366
|
+
// G2 的修法不是把 `Name` 加回词表(那是按【名字】猜语义,G-C 已经为这份
|
|
367
|
+
// 宽松付过一次代价:fr-007 pre 侧召回归零),而是看**被调用方函数体内
|
|
368
|
+
// 到底有没有校验证据**。
|
|
369
|
+
//
|
|
370
|
+
// 精度取舍:净化作用在【表达式】上,不是整函数 —— 同函数体里另一条未被
|
|
371
|
+
// 校验的流仍会标记。这是 G2 相对 G1(函数级 selfGuarded)的收窄。
|
|
372
|
+
const body = (guardCall) => `
|
|
373
|
+
import * as fs from "fs";
|
|
374
|
+
export function read(req: any) {
|
|
375
|
+
const name = req.params.name;
|
|
376
|
+
${guardCall} return fs.readFileSync("/data/" + name, "utf-8");
|
|
377
|
+
}
|
|
378
|
+
function assertTemplateName(name: string): void {
|
|
379
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
|
|
380
|
+
}
|
|
381
|
+
`;
|
|
382
|
+
(0, vitest_1.it)("正对照:去掉自定义校验调用后必须标记", () => {
|
|
383
|
+
const dir = makeProject({ "a.ts": body("") });
|
|
384
|
+
try {
|
|
385
|
+
(0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
(0, vitest_1.it)("自定义校验函数(名字不含路径语义后缀)被调用 → 不得标记", () => {
|
|
392
|
+
const dir = makeProject({ "a.ts": body(" assertTemplateName(name);\n") });
|
|
393
|
+
try {
|
|
394
|
+
(0, vitest_1.expect)(marksFor(dir, "read")).not.toContain(PATH_MARK);
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
(0, vitest_1.it)("负对照:被调用方体内**没有**校验证据时,调用它不得抑制(不能见调用就净化)", () => {
|
|
401
|
+
const dir = makeProject({
|
|
402
|
+
"a.ts": `
|
|
403
|
+
import * as fs from "fs";
|
|
404
|
+
export function read(req: any) {
|
|
405
|
+
const name = req.params.name;
|
|
406
|
+
logIt(name);
|
|
407
|
+
return fs.readFileSync("/data/" + name, "utf-8");
|
|
408
|
+
}
|
|
409
|
+
function logIt(x: string): void { console.log(x); }
|
|
410
|
+
`,
|
|
411
|
+
});
|
|
412
|
+
try {
|
|
413
|
+
(0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
(0, vitest_1.it)("表达式级净化:同函数体里另一条未被校验的流仍要标记", () => {
|
|
420
|
+
// G1 的函数级 selfGuarded 会把整个函数体一起静默;G2 只净化真正被传进
|
|
421
|
+
// 守卫调用的表达式 —— 这条用例锁住这个差别。
|
|
422
|
+
const dir = makeProject({
|
|
423
|
+
"a.ts": `
|
|
424
|
+
import * as fs from "fs";
|
|
425
|
+
export function read(req: any) {
|
|
426
|
+
const name = req.params.name;
|
|
427
|
+
const other = req.params.other;
|
|
428
|
+
assertTemplateName(name);
|
|
429
|
+
fs.readFileSync("/data/" + name, "utf-8");
|
|
430
|
+
return fs.writeFileSync("/data/" + other, "x");
|
|
431
|
+
}
|
|
432
|
+
function assertTemplateName(name: string): void {
|
|
433
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
|
|
434
|
+
}
|
|
435
|
+
`,
|
|
436
|
+
});
|
|
437
|
+
try {
|
|
438
|
+
(0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
(0, vitest_1.it)("跨函数(taintpath_B 形态):净化只发生在调用点,被调用方定义照旧被反标", () => {
|
|
445
|
+
// 语料里 `loadTemplate` 自身不含任何不可信根,它被标记是因为调用点命中了
|
|
446
|
+
// 跨函数一跳(onMethodHit 反标)。故本用例在同一文件里放两个几乎相同的
|
|
447
|
+
// 调用点 —— 一个净化、一个不净化 —— 才能同时锁住两侧。
|
|
448
|
+
const dir = makeProject({
|
|
449
|
+
"store.ts": `
|
|
450
|
+
import * as fs from "fs";
|
|
451
|
+
export function loadTemplate(p: string): string {
|
|
452
|
+
return fs.readFileSync("/srv/templates/" + p, "utf-8");
|
|
453
|
+
}
|
|
454
|
+
`,
|
|
455
|
+
"handler.ts": `
|
|
456
|
+
import { loadTemplate } from "./store";
|
|
457
|
+
export function dispatchToolGuarded(params: any) {
|
|
458
|
+
const args = params.arguments;
|
|
459
|
+
assertTemplateName(args.name);
|
|
460
|
+
return loadTemplate(args.name);
|
|
461
|
+
}
|
|
462
|
+
export function dispatchToolBare(params: any) {
|
|
463
|
+
const args = params.arguments;
|
|
464
|
+
return loadTemplate(args.name);
|
|
465
|
+
}
|
|
466
|
+
function assertTemplateName(name: string): void {
|
|
467
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
|
|
468
|
+
}
|
|
469
|
+
`,
|
|
470
|
+
});
|
|
471
|
+
try {
|
|
472
|
+
// 净化侧:污点表达式已被校验 ⇒ 调用点不标记
|
|
473
|
+
(0, vitest_1.expect)(marksFor(dir, "dispatchToolGuarded")).not.toContain(PATH_MARK);
|
|
474
|
+
// 正对照:同形状去掉校验 ⇒ 照旧标记,且被调用方被反标
|
|
475
|
+
(0, vitest_1.expect)(marksFor(dir, "dispatchToolBare")).toContain(PATH_MARK);
|
|
476
|
+
(0, vitest_1.expect)(marksFor(dir, "loadTemplate")).toContain(PATH_MARK);
|
|
477
|
+
}
|
|
478
|
+
finally {
|
|
479
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
(0, vitest_1.it)("已知缺口 G2b:经项目自有 helper 转手的校验证据不生效(保守边界,仍标记)", () => {
|
|
483
|
+
// 只认 tier-0(函数体自身含校验证据)。经 helper 转手 ⇒ 那是【推断】出来
|
|
484
|
+
// 的守卫,拿推断结果做抑制会把推断误差直接放大成误报消除。
|
|
485
|
+
// 代价:这类形态仍会误报,与 C4b(helper 不传播)是同一族缺口。
|
|
486
|
+
const dir = makeProject({
|
|
487
|
+
"a.ts": `
|
|
488
|
+
import * as fs from "fs";
|
|
489
|
+
export function read(req: any) {
|
|
490
|
+
const name = req.params.name;
|
|
491
|
+
checkName(name);
|
|
492
|
+
return fs.readFileSync("/data/" + name, "utf-8");
|
|
493
|
+
}
|
|
494
|
+
function checkName(name: string): void {
|
|
495
|
+
assertTemplateName(name);
|
|
496
|
+
}
|
|
497
|
+
function assertTemplateName(name: string): void {
|
|
498
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) throw new Error("bad template name");
|
|
499
|
+
}
|
|
500
|
+
`,
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
(0, vitest_1.expect)(marksFor(dir, "read")).toContain(PATH_MARK);
|
|
504
|
+
}
|
|
505
|
+
finally {
|
|
506
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
/**
|
|
511
|
+
* ── fr-016 post 侧的压制不是同义反复(2026-09-19,3.7.38)──
|
|
512
|
+
*
|
|
513
|
+
* 疑虑是合理的:G-C 的后缀表里 `Within` 这一项,当初就是照着 fr-016 的
|
|
514
|
+
* `assertWithinDir` 加进去的。那么 fr-016 的 post=0 到底算不算证据?
|
|
515
|
+
*
|
|
516
|
+
* 区分办法:把守卫函数名换成**不含任何 G-C 后缀**的名字( enforces 都不带
|
|
517
|
+
* assert/ensure/check/validate 前缀,这里用 `containCheck` 与 `stamp`),
|
|
518
|
+
* 只保留它函数体内的真实包含性校验:
|
|
519
|
+
* const base = path.resolve(baseDir);
|
|
520
|
+
* if (!path.resolve(targetPath).startsWith(base + path.sep)) throw …
|
|
521
|
+
* 若仍被压制,说明压制的依据是被调用方**自身的证据**(G2 的 tier-0),
|
|
522
|
+
* 不是名字;那么 fr-016 的 post=0 就不是用测试集反推出来的同义反复。
|
|
523
|
+
* 正对照:同形状把函数体里的 startWith 删掉,必须重新标记。
|
|
524
|
+
*/
|
|
525
|
+
(0, vitest_1.describe)("fr-016 的压制依据是被调用方的证据,不是它的名字", () => {
|
|
526
|
+
const body = (guardName, keepGuard) => [
|
|
527
|
+
'import * as fs from "fs";',
|
|
528
|
+
'import * as path from "path";',
|
|
529
|
+
`function ${guardName}(baseDir: string, targetPath: string): void {`,
|
|
530
|
+
" const base = path.resolve(baseDir);",
|
|
531
|
+
" const target = path.resolve(targetPath);",
|
|
532
|
+
keepGuard
|
|
533
|
+
? ' if (target !== base && !target.startsWith(base + path.sep)) throw new Error("outside");'
|
|
534
|
+
: " // 故意不校验:这里是正对照,删掉守卫后必须重新标记",
|
|
535
|
+
"}",
|
|
536
|
+
"export function emit(doc: Record<string, any>, outDir: string) {",
|
|
537
|
+
" for (const channelName of Object.keys(doc)) {",
|
|
538
|
+
" const channelFile = `${outDir}/${channelName}.yaml`;",
|
|
539
|
+
` ${guardName}(outDir, channelFile);`,
|
|
540
|
+
' fs.writeFileSync(channelFile, "x");',
|
|
541
|
+
" }",
|
|
542
|
+
"}",
|
|
543
|
+
].join("\n");
|
|
544
|
+
(0, vitest_1.it)("负向:守卫函数名不含 G-C 后缀,但其函数体含包含性校验 ⇒ 仍不标记", () => {
|
|
545
|
+
const dir = makeProject({ "a.ts": body("stamp", true) });
|
|
546
|
+
try {
|
|
547
|
+
(0, vitest_1.expect)(marksFor(dir, "emit")).not.toContain(PATH_MARK);
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
551
|
+
}
|
|
552
|
+
}, 150000);
|
|
553
|
+
(0, vitest_1.it)("正对照:同形状删掉函数体里的 startsWith ⇒ 必须重新标记(证明上一条不是空过)", () => {
|
|
554
|
+
const dir = makeProject({ "a.ts": body("stamp", false) });
|
|
555
|
+
try {
|
|
556
|
+
(0, vitest_1.expect)(marksFor(dir, "emit")).toContain(PATH_MARK);
|
|
557
|
+
}
|
|
558
|
+
finally {
|
|
559
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
560
|
+
}
|
|
561
|
+
}, 150000);
|
|
562
|
+
});
|
|
359
563
|
/**
|
|
360
564
|
* ── 已知边界(本次写用例时实测踩到,记录以免后人重复踩)──
|
|
361
565
|
*
|