progmune-runtime 3.7.35 → 3.7.36
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 +48 -0
- package/dist/extract-ir-taint-structural.test.js +156 -0
- package/dist/extract-ir.js +104 -5
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.36] — 2026-09-19
|
|
4
|
+
|
|
5
|
+
### C4:污点经路径塑形表达式包装后仍传播
|
|
6
|
+
|
|
7
|
+
缺口实证(taintpath_A,measured):`readBasename` / `readResolveOnly` /
|
|
8
|
+
`readJoinWrapped` / `readNormalizeWrapped` 四条语义上应标记、实测不标记——
|
|
9
|
+
传播只认 `x = <污点>` 直赋,而 `path.join/resolve/normalize/basename` 是真实
|
|
10
|
+
工程构造路径的**默认写法**,这条断链等于把最常见形态整片漏掉。
|
|
11
|
+
|
|
12
|
+
政策是**白名单传播**,不是「RHS 含污点就传播」:
|
|
13
|
+
- 只经 path.* 家族与保值的字符串方法(trim/replace/…)传播;拼接与模板字面量
|
|
14
|
+
同样只塑形、不改来源;有界迭代 3 跳(`normalize → join` 这类链式能接上)
|
|
15
|
+
- **认不出的调用不传播** ⇒ `const safe = sanitizeName(p)` 天然不污染。
|
|
16
|
+
未知函数默认站在精度一侧——这是白名单相对黑名单的决定性优势
|
|
17
|
+
- 代价(记为缺口 C4b):项目自有 helper(如 `buildPath(p)`)仍不传播
|
|
18
|
+
|
|
19
|
+
### 两处必须一并改的「假通过」(C4 把它们顶出来了)
|
|
20
|
+
|
|
21
|
+
C4 之前,下列用例的「不标记」**不是因为判别力对,而是污点根本没走到 sink**
|
|
22
|
+
——方法学规则 R7 的第二种形态。C4 一放开它们就会翻成误报:
|
|
23
|
+
|
|
24
|
+
1. **G-A2 漏 `startsWith(base)`**:原正则要求标识符在 base/root/dir 之外还有
|
|
25
|
+
前导字符,最朴素的 `target.startsWith(base)` 匹配不上。改为取出整个实参再
|
|
26
|
+
判定,并加 `database` 等反例名单(`database` 以 base 结尾但与目录无关)
|
|
27
|
+
2. **G-A2 漏字符串字面量形态**:`startsWith("/srv/data")` 同样匹配不上。
|
|
28
|
+
现支持绝对路径字面量(长度 >1,排除 `startsWith("/")`——那只是判绝对路径)
|
|
29
|
+
|
|
30
|
+
### 闸门改为实时提取(第二处空过)
|
|
31
|
+
|
|
32
|
+
`check-taintpath.ts` 原默认读 `reports/batch-scan-results.json`——那是别人跑
|
|
33
|
+
batch-scan 留下的**陈旧产物**。实测踩到:C4 落地后 4 条 known-gap 已翻正,
|
|
34
|
+
闸门仍报「未标记,符合预期」。已改为默认对 `generated/taintpath_*` 实时跑
|
|
35
|
+
extractIR(需走报告时显式 `--report`)。
|
|
36
|
+
|
|
37
|
+
### 验收
|
|
38
|
+
|
|
39
|
+
- taintpath 闸门 **24/24,失败 0、缺口闭合待更新 0**;4 条 known-gap 转 mark
|
|
40
|
+
- fr-007 openhop 真实语料维持 **pre 5 / post 0** —— C4 只补召回,未削弱 G1 判别力
|
|
41
|
+
- `extract-ir-taint-structural.test.ts` 新增 C4 组 11 条(7 正 + 4 负对照,R6)
|
|
42
|
+
- 反向验证:回退 v3.7.35 后 **7 条正例全失败**,4 条负对照仍绿
|
|
43
|
+
- 全组回归 72 passed;`tsc -p tsconfig.json` 零错误
|
|
44
|
+
|
|
45
|
+
### 实现期踩的一个坑(已入注释)
|
|
46
|
+
|
|
47
|
+
`taintedViaShaper` 最初写了 `if (tainted.size === 0) return`——而这 4 条
|
|
48
|
+
known-gap 的共同形态恰恰是「全函数没有任何直赋污点」,等于整条 C4 不生效。
|
|
49
|
+
种子是根模式(taintPattern 恒定并入 UNTRUSTED_ROOT_SRC),不该依赖已有污点。
|
|
50
|
+
|
|
3
51
|
## [3.7.35] — 2026-09-19
|
|
4
52
|
|
|
5
53
|
### 重建盲测覆盖:taintpath 语料族(TS 795 空过的终结)
|
|
@@ -287,3 +287,159 @@ export function seedFlow() {
|
|
|
287
287
|
}, 150000);
|
|
288
288
|
}
|
|
289
289
|
});
|
|
290
|
+
// ═══════════════════════════════════════════════════════════════
|
|
291
|
+
// C4(2026-09-19):污点经「路径塑形表达式」包装后仍应传播
|
|
292
|
+
//
|
|
293
|
+
// 缺口实证:taintpath_A 的 readBasename / readResolveOnly / readJoinWrapped /
|
|
294
|
+
// readNormalizeWrapped 四条,语义上应标记、实测不标记 —— 传播只认 `x = <污点>`
|
|
295
|
+
// 直赋,而 `path.join/resolve/normalize/basename` 是真实工程构造路径的**默认
|
|
296
|
+
// 写法**,这条断链等于把最常见的形态整片漏掉。
|
|
297
|
+
//
|
|
298
|
+
// 政策是**白名单传播**:只经 path.* 家族与保值的字符串方法传播,认不出的调用
|
|
299
|
+
// 不传播。于是 `const safe = sanitizeName(p)` 天然不污染 —— 未知函数默认站在
|
|
300
|
+
// 精度一侧。代价是项目自有 helper(buildPath(p))仍不传播(缺口 C4b)。
|
|
301
|
+
// ═══════════════════════════════════════════════════════════════
|
|
302
|
+
(0, vitest_1.describe)("C4:污点经路径塑形表达式包装后仍传播", () => {
|
|
303
|
+
const cases = [
|
|
304
|
+
{
|
|
305
|
+
name: "path.join 包装",
|
|
306
|
+
fn: "readJoin",
|
|
307
|
+
code: `export function readJoin(req: any) {
|
|
308
|
+
const target = path.join("/data", req.params.name);
|
|
309
|
+
return fs.readFileSync(target, "utf-8");
|
|
310
|
+
}`,
|
|
311
|
+
expectMark: true,
|
|
312
|
+
why: "最基础的塑形包装:join(root, taint) 不得断链",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "path.normalize 包装",
|
|
316
|
+
fn: "readNormalize",
|
|
317
|
+
code: `export function readNormalize(req: any) {
|
|
318
|
+
const target = path.normalize(req.params.name);
|
|
319
|
+
return fs.readFileSync(target, "utf-8");
|
|
320
|
+
}`,
|
|
321
|
+
expectMark: true,
|
|
322
|
+
why: "normalize 只规范化、不改变来源",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "path.basename 包装(N-A)",
|
|
326
|
+
fn: "readBasename",
|
|
327
|
+
code: `export function readBasename(req: any) {
|
|
328
|
+
const name = path.basename(req.params.name);
|
|
329
|
+
return fs.readFileSync("/data/" + name, "utf-8");
|
|
330
|
+
}`,
|
|
331
|
+
expectMark: true,
|
|
332
|
+
why: "N-A:basename 不算守卫(fr-012 pre 实测反例),包装后污点仍须流动",
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "resolve 无包含性比较(N-C)",
|
|
336
|
+
fn: "readResolveOnly",
|
|
337
|
+
code: `export function readResolveOnly(req: any) {
|
|
338
|
+
const base = path.resolve("/data");
|
|
339
|
+
const target = path.resolve(base, req.params.name);
|
|
340
|
+
return fs.readFileSync(target, "utf-8");
|
|
341
|
+
}`,
|
|
342
|
+
expectMark: true,
|
|
343
|
+
why: "N-C:只有 resolve、没有 startsWith(base) 比较,不算守卫",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "链式两跳塑形",
|
|
347
|
+
fn: "readChained",
|
|
348
|
+
code: `export function readChained(req: any) {
|
|
349
|
+
const norm = path.normalize(req.params.dir);
|
|
350
|
+
const target = path.join(norm, "index.html");
|
|
351
|
+
return fs.readFileSync(target, "utf-8");
|
|
352
|
+
}`,
|
|
353
|
+
expectMark: true,
|
|
354
|
+
why: "fr-012 下载侧就是这个形状:normalize → join 两跳,须接上",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
name: "模板字面量拼接",
|
|
358
|
+
fn: "readTemplate",
|
|
359
|
+
code: `export function readTemplate(req: any) {
|
|
360
|
+
const target = \`/data/\${req.params.name}\`;
|
|
361
|
+
return fs.readFileSync(target, "utf-8");
|
|
362
|
+
}`,
|
|
363
|
+
expectMark: true,
|
|
364
|
+
why: "模板字面量只塑形、不改来源",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "字符串方法 trim(须先经 C4 才能到这一步)",
|
|
368
|
+
fn: "readTrim",
|
|
369
|
+
// 注意形态:不能写成 `const n = req.params.name.trim()` —— 那样根就在
|
|
370
|
+
// 开头、旧版也能标记,用例就失去判别力了(反向验证时实测通过,白锁一场)。
|
|
371
|
+
// 必须先经一次塑形调用,trim 才是 C4 链条上的第二跳。
|
|
372
|
+
code: `export function readTrim(req: any) {
|
|
373
|
+
const normalized = path.normalize(req.params.name);
|
|
374
|
+
const trimmed = normalized.trim();
|
|
375
|
+
return fs.readFileSync("/data/" + trimmed, "utf-8");
|
|
376
|
+
}`,
|
|
377
|
+
expectMark: true,
|
|
378
|
+
why: "保值的字符串方法不得断链;本条同时验证 C4 的迭代(normalize → trim 两跳)",
|
|
379
|
+
},
|
|
380
|
+
// ── 负向:每一条都有同形状的正对照(方法学规则 R6)──
|
|
381
|
+
{
|
|
382
|
+
name: "负对照:守卫仍在(startsWith(base) 形态)",
|
|
383
|
+
fn: "readGuardWithinBase",
|
|
384
|
+
code: `export function readGuardWithinBase(req: any) {
|
|
385
|
+
const base = path.resolve("/data");
|
|
386
|
+
const target = path.resolve(base, req.params.name);
|
|
387
|
+
if (!target.startsWith(base)) throw new Error("outside");
|
|
388
|
+
return fs.readFileSync(target, "utf-8");
|
|
389
|
+
}`,
|
|
390
|
+
expectMark: false,
|
|
391
|
+
why: "正对照 = readResolveOnly(同形状去掉 startsWith 必须重新报出)",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "负对照:常量路径不因塑形而标记",
|
|
395
|
+
fn: "readConstantJoin",
|
|
396
|
+
code: `export function readConstantJoin() {
|
|
397
|
+
const target = path.join("/data", "index.html");
|
|
398
|
+
return fs.readFileSync(target, "utf-8");
|
|
399
|
+
}`,
|
|
400
|
+
expectMark: false,
|
|
401
|
+
why: "正对照 = readJoin(把 \"index.html\" 换成 req.params.name 必须报出)",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "负对照:未知 helper 不传播(白名单的精度代价,缺口 C4b)",
|
|
405
|
+
fn: "readUnknownHelper",
|
|
406
|
+
code: `function buildPath(name: string): string {
|
|
407
|
+
return "/data/" + name;
|
|
408
|
+
}
|
|
409
|
+
export function readUnknownHelper(req: any) {
|
|
410
|
+
const target = buildPath(req.params.name);
|
|
411
|
+
return fs.readFileSync(target, "utf-8");
|
|
412
|
+
}`,
|
|
413
|
+
expectMark: false,
|
|
414
|
+
why: "白名单外一律不传播(精度优先);正对照 = readJoin 必须报出",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: "负对照:净化函数不传播",
|
|
418
|
+
fn: "readSanitized",
|
|
419
|
+
code: `function sanitizeName(name: string): string {
|
|
420
|
+
return name.replace(/[^a-z0-9]/gi, "");
|
|
421
|
+
}
|
|
422
|
+
export function readSanitized(req: any) {
|
|
423
|
+
const safe = sanitizeName(req.params.name);
|
|
424
|
+
return fs.readFileSync("/data/" + safe, "utf-8");
|
|
425
|
+
}`,
|
|
426
|
+
expectMark: false,
|
|
427
|
+
why: "未知(疑似净化)调用默认不传播——这是白名单相对黑名单的决定性优势",
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
for (const c of cases) {
|
|
431
|
+
(0, vitest_1.it)(`${c.name} —— ${c.why}`, () => {
|
|
432
|
+
const dir = makeProject({ "a.ts": `import * as fs from "fs";\nimport * as path from "path";\n\n${c.code}\n` });
|
|
433
|
+
try {
|
|
434
|
+
const marks = marksFor(dir, c.fn);
|
|
435
|
+
if (c.expectMark)
|
|
436
|
+
(0, vitest_1.expect)(marks).toContain(PATH_MARK);
|
|
437
|
+
else
|
|
438
|
+
(0, vitest_1.expect)(marks).not.toContain(PATH_MARK);
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
442
|
+
}
|
|
443
|
+
}, 150000);
|
|
444
|
+
}
|
|
445
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -378,6 +378,21 @@ function extractDirectCalls(func, preText) {
|
|
|
378
378
|
// 与 protocol-detector 的同名规则(source-level benchmark 路径)。
|
|
379
379
|
// ═══════════════════════════════════════════════════════════════
|
|
380
380
|
const PATH_TRAVERSAL_MARKER = "__progmune_path_traversal__";
|
|
381
|
+
/**
|
|
382
|
+
* 标识符是否「看起来像基目录」—— G-A2 的定案依据。
|
|
383
|
+
*
|
|
384
|
+
* 判定:以 base / root / dir 结尾(大小写与 camel、下划线均可),
|
|
385
|
+
* 前缀任意(`base` / `baseDir` / `allowedRoot` / `root_dir` 都算)。
|
|
386
|
+
*
|
|
387
|
+
* 反例名单是实证加的:`database` 以 base 结尾但与目录无关,若不排除,
|
|
388
|
+
* 一旦有人写 `p.startsWith(database)` 就会被当成守卫 —— 那是精度漏洞。
|
|
389
|
+
*/
|
|
390
|
+
const BASE_DIR_IDENT_DENY = /^(?:database|databases|codebase|knowledgebase|base64|basename|dirname|basenames)$/i;
|
|
391
|
+
function isBaseDirIdent(id) {
|
|
392
|
+
if (BASE_DIR_IDENT_DENY.test(id))
|
|
393
|
+
return false;
|
|
394
|
+
return /^(?:[A-Za-z_$][\w$]*)?(?:base|root|dir)$/i.test(id);
|
|
395
|
+
}
|
|
381
396
|
const PATH_GUARD_RULES = [
|
|
382
397
|
{
|
|
383
398
|
id: "G-A",
|
|
@@ -388,9 +403,35 @@ const PATH_GUARD_RULES = [
|
|
|
388
403
|
},
|
|
389
404
|
{
|
|
390
405
|
id: "G-A2",
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
406
|
+
// 粗筛:任意 `.startsWith(<ident>)`;定案交给 decide —— 实参名是否像基目录。
|
|
407
|
+
re: /\.\s*startsWith\s*\(\s*([^)]*?)\s*\)/,
|
|
408
|
+
// 2026-09-19 两次修订,都是被 C4「顶出来」的真问题(C4 之前相关用例
|
|
409
|
+
// 的「不标记」是空过——污点根本没走到 sink,见方法学规则 R7):
|
|
410
|
+
// ① 原正则 `[A-Za-z_$][\w$]*(?:[Bb]ase|…)` 要求标识符在 base/root/dir
|
|
411
|
+
// **之外**还至少有一个前导字符,于是最朴素的 `startsWith(base)`
|
|
412
|
+
// (taintpath_A readGuardWithin)匹配不上;
|
|
413
|
+
// ② `startsWith("/srv/data")` —— 基目录写成**字符串字面量**——同样匹配
|
|
414
|
+
// 不上(C5 负对照 readInlineGuarded 就是这形态)。
|
|
415
|
+
// 故改为:先取出 startsWith 的整个实参,再判定它是「基目录名」还是
|
|
416
|
+
// 「绝对路径字面量」。
|
|
417
|
+
decide: (text) => {
|
|
418
|
+
const re = /\.\s*startsWith\s*\(\s*([^)]*?)\s*\)/g;
|
|
419
|
+
let m;
|
|
420
|
+
while ((m = re.exec(text)) !== null) {
|
|
421
|
+
const arg = m[1] || "";
|
|
422
|
+
// 形参里的标识符:`base` / `base + path.sep` / `rootDir + "/"`
|
|
423
|
+
const idm = /([A-Za-z_$][\w$]*)/.exec(arg);
|
|
424
|
+
if (idm && isBaseDirIdent(idm[1]))
|
|
425
|
+
return true;
|
|
426
|
+
// 绝对路径字面量:`"/srv/data"`。长度 >1 是为了排除 `startsWith("/")`
|
|
427
|
+
// —— 那只是「是否绝对路径」,不构成包含性。
|
|
428
|
+
if (/["'`]\/[^"'`]+["'`]/.test(arg))
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
},
|
|
433
|
+
why: "目录包含性校验(基目录名或绝对路径字面量):startsWith(base / baseDir / \"/srv/data\" / base + sep)",
|
|
434
|
+
source: "设计形态,canonical;2026-09-19 补 `base` 单标识符(taintpath_A readGuardWithin)与绝对路径字面量(C5 负对照 readInlineGuarded)",
|
|
394
435
|
},
|
|
395
436
|
{
|
|
396
437
|
id: "G-B1",
|
|
@@ -456,7 +497,7 @@ function callsPathGuardFn(text) {
|
|
|
456
497
|
/** 返回命中的守卫规则 id;无校验证据返回 null。 */
|
|
457
498
|
function hasPathGuardEvidence(text) {
|
|
458
499
|
for (const r of PATH_GUARD_RULES) {
|
|
459
|
-
if (r.re.test(text))
|
|
500
|
+
if (r.decide ? r.decide(text) : r.re.test(text))
|
|
460
501
|
return r.id;
|
|
461
502
|
}
|
|
462
503
|
if (callsPathGuardFn(text))
|
|
@@ -569,7 +610,29 @@ function hasRequestRootedExpr(text) {
|
|
|
569
610
|
function tsSinkCallRegex() {
|
|
570
611
|
return new RegExp(`(?:^|[^\\w.$])(?:await\\s+)?(?:[\\w$]+\\.)?(${TS_FILE_SINK_NAMES.join("|")})\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
|
|
571
612
|
}
|
|
572
|
-
/**
|
|
613
|
+
/**
|
|
614
|
+
* C4(2026-09-19):污点经「路径塑形表达式」包装后仍然传播。
|
|
615
|
+
*
|
|
616
|
+
* 缺口实证(taintpath_A,2026-09-19 measured):
|
|
617
|
+
* `const target = path.join(ROOT, req.params.name); fs.readFileSync(target)`
|
|
618
|
+
* 不标记 —— 因为传播只认 `x = <污点>` 直赋,`path.join(...)` 把流掐断了。
|
|
619
|
+
* 真实工程里 `join/resolve/normalize/basename` 是路径构造的**默认写法**,
|
|
620
|
+
* 这条断链等于把最常见的形态整片漏掉。
|
|
621
|
+
*
|
|
622
|
+
* 政策:**白名单传播**,不是「RHS 含污点就传播」。
|
|
623
|
+
* - 只经【路径塑形】的调用传播(path.* 家族 + 保值的字符串方法);
|
|
624
|
+
* - 字符串拼接与模板字面量同样只塑形、不改来源,故一并传播;
|
|
625
|
+
* - 认不出的调用**不传播** —— 于是 `const safe = sanitizeName(p)` 天然
|
|
626
|
+
* 不污染。这是白名单相对黑名单的决定性优势:未知函数默认站在精度一侧。
|
|
627
|
+
* 代价:项目自有 helper(如 `buildPath(p)`)仍不传播,记为 C4b 缺口。
|
|
628
|
+
*
|
|
629
|
+
* 已知限制:只扫单行赋值(`const x = …;` 一行写完)。跨行书写的调用
|
|
630
|
+
* 链不传播——保持简单,等 C4b 一并处理。
|
|
631
|
+
*/
|
|
632
|
+
const TS_PATH_SHAPER_RE = /(?:path\s*\.\s*(?:posix|win32)\s*\.\s*|path\s*\.\s*)?\b(?:join|resolve|normalize|basename|dirname|extname|relative|format|toNamespacedPath)\s*\(|\.\s*(?:trim|trimStart|trimEnd|replace|replaceAll|toLowerCase|toUpperCase|toString|slice|substring|substr|padStart|padEnd|concat|normalize)\s*\(/;
|
|
633
|
+
/** 拼接形态:`+` 或反引号(模板字面量)—— 同样只是塑形 */
|
|
634
|
+
const TS_CONCAT_RE = /\+|`/;
|
|
635
|
+
/** 函数体内被不可信根污染的局部名(含解构、单跳赋值与 C4 塑形传播,深度 ≤3) */
|
|
573
636
|
function collectTaintedNames(text) {
|
|
574
637
|
const tainted = new Set();
|
|
575
638
|
let m;
|
|
@@ -588,6 +651,7 @@ function collectTaintedNames(text) {
|
|
|
588
651
|
const bareAssign = new RegExp(`(?:^|[^\\w$.])([\\w$]+)\\s*=\\s*(?:await\\s+)?(?:${root})`, "g");
|
|
589
652
|
while ((m = bareAssign.exec(text)) !== null)
|
|
590
653
|
tainted.add(m[1]);
|
|
654
|
+
taintedViaShaper(text, tainted);
|
|
591
655
|
// 单跳传播(深度 ≤2)
|
|
592
656
|
for (let depth = 0; depth < 2; depth++) {
|
|
593
657
|
if (tainted.size === 0)
|
|
@@ -606,6 +670,41 @@ function collectTaintedNames(text) {
|
|
|
606
670
|
}
|
|
607
671
|
return tainted;
|
|
608
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* C4:把「经路径塑形表达式包装」的赋值名补进污点集合(有界迭代 ≤3 轮)。
|
|
675
|
+
*
|
|
676
|
+
* 按行扫描 `[(const|let|var)] NAME = RHS`:RHS 含污点证据 且
|
|
677
|
+
* (含塑形调用 或 含拼接/模板)⇒ NAME 污染。迭代是为了让
|
|
678
|
+
* `normalized = path.normalize(p)` → `savePath = path.join(normalized, f)`
|
|
679
|
+
* 这类链式构造能接上(fr-012 下载侧守卫块就是这个形状)。
|
|
680
|
+
*/
|
|
681
|
+
function taintedViaShaper(text, tainted) {
|
|
682
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
683
|
+
// 注意:这里**不能**以 `tainted.size === 0` 提前退出 —— 种子是根模式
|
|
684
|
+
// (taintPattern 恒定并入 UNTRUSTED_ROOT_SRC)。本轮这 4 条 known-gap
|
|
685
|
+
// 的共同形态恰恰是「全函数没有任何直赋污点,根只出现在塑形调用里」,
|
|
686
|
+
// 加这道闸门等于整条 C4 不生效(2026-09-19 实测踩到)。
|
|
687
|
+
const taintRe = taintPattern(tainted);
|
|
688
|
+
if (!taintRe)
|
|
689
|
+
return;
|
|
690
|
+
let added = false;
|
|
691
|
+
for (const line of text.split("\n")) {
|
|
692
|
+
const m = /^\s*(?:const|let|var)?\s*([\w$]+)\s*=\s*(?:await\s+)?(.+)$/.exec(line);
|
|
693
|
+
if (!m)
|
|
694
|
+
continue;
|
|
695
|
+
const name = m[1];
|
|
696
|
+
const rhs = m[2];
|
|
697
|
+
if (tainted.has(name))
|
|
698
|
+
continue;
|
|
699
|
+
if (taintRe.test(rhs) && (TS_PATH_SHAPER_RE.test(rhs) || TS_CONCAT_RE.test(rhs))) {
|
|
700
|
+
tainted.add(name);
|
|
701
|
+
added = true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!added)
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
609
708
|
function taintPattern(tainted) {
|
|
610
709
|
const parts = [UNTRUSTED_ROOT_SRC];
|
|
611
710
|
if (tainted.size > 0) {
|
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.36";
|
|
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.36",
|
|
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/",
|