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/dist/extract-ir.js
CHANGED
|
@@ -516,15 +516,20 @@ function escapeRe(s) {
|
|
|
516
516
|
*/
|
|
517
517
|
function pathGuardFunctionNames(project) {
|
|
518
518
|
const guarded = new Set();
|
|
519
|
+
/** tier-0:函数体自身含校验证据的名字(G1 原语义) */
|
|
520
|
+
const direct = new Set();
|
|
519
521
|
const bodies = [];
|
|
520
522
|
const note = (names, text) => {
|
|
521
523
|
if (!text)
|
|
522
524
|
return;
|
|
523
525
|
bodies.push({ names, text });
|
|
524
|
-
if (hasPathGuardEvidence(text))
|
|
526
|
+
if (hasPathGuardEvidence(text)) {
|
|
525
527
|
for (const n of names)
|
|
526
|
-
if (n)
|
|
528
|
+
if (n) {
|
|
527
529
|
guarded.add(n);
|
|
530
|
+
direct.add(n);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
528
533
|
};
|
|
529
534
|
for (const sf of project.getSourceFiles()) {
|
|
530
535
|
for (const cls of sf.getClasses()) {
|
|
@@ -567,7 +572,286 @@ function pathGuardFunctionNames(project) {
|
|
|
567
572
|
if (!added)
|
|
568
573
|
break;
|
|
569
574
|
}
|
|
570
|
-
return guarded;
|
|
575
|
+
return { all: guarded, direct };
|
|
576
|
+
}
|
|
577
|
+
// ═══════════════════════════════════════════════════════════════
|
|
578
|
+
// G2(2026-09-19):调用点抑制 —— 污点【表达式级】净化
|
|
579
|
+
// ═══════════════════════════════════════════════════════════════
|
|
580
|
+
// 缺口实证(taintpath_B,2026-09-19 measured):
|
|
581
|
+
// `assertTemplateName(args.name); return loadTemplate(args.name);`
|
|
582
|
+
// 仍被标记。`assertTemplateName` 的函数体内是锚定字符集白名单(G-D),
|
|
583
|
+
// 是**真的**校验,但它的名字不含路径语义后缀 —— `Name` 在修 `ensureDir()`
|
|
584
|
+
// 误判时被整体移出了 G-C 守卫后缀表,于是调用点侧认不出来。
|
|
585
|
+
//
|
|
586
|
+
// 为什么不直接放宽后缀表:那是按【名字】猜语义,会把判别力稀释成猜函数名
|
|
587
|
+
// (G-C 的实证代价已经量过一次:放宽 ⇒ fr-007 pre 侧召回归零)。
|
|
588
|
+
// 本实现改为按【被调用方的实际证据】定案:
|
|
589
|
+
// 函数体内调用了 tier-0 守卫函数(自身含校验证据),且污点表达式被当作
|
|
590
|
+
// 实参传进去 ⇒ 该表达式视为已净化,再流进 sink 不算污点。
|
|
591
|
+
//
|
|
592
|
+
// 精度取舍(关键点):净化作用在【表达式】上,不是整函数。
|
|
593
|
+
// G1 的 selfGuarded 是函数级的(一个校验词汇压掉整个函数体所有流);
|
|
594
|
+
// G2 只净化真正被传进守卫调用的那个表达式 —— 同函数体里另一条未被校验
|
|
595
|
+
// 的流仍会标记。这是 G2 相对 G1 的收窄,而不是又一次放宽。
|
|
596
|
+
// ═══════════════════════════════════════════════════════════════
|
|
597
|
+
/**
|
|
598
|
+
* 收集函数体内「被传进 tier-0 守卫函数」的实参表达式,编译成一个判定正则。
|
|
599
|
+
*
|
|
600
|
+
* 边界:只收**简单取值表达式**(标识符 / 成员 / 下标 / 字符串字面量),
|
|
601
|
+
* 复杂表达式(模板串、拼接、调用)不收 —— 那些形态无法在 sink 侧可靠比对。
|
|
602
|
+
*/
|
|
603
|
+
function collectSanitizedExprs(text, directGuardFns) {
|
|
604
|
+
if (!directGuardFns || directGuardFns.size === 0)
|
|
605
|
+
return null;
|
|
606
|
+
const names = [...directGuardFns].filter((n) => !n.includes("."));
|
|
607
|
+
if (names.length === 0)
|
|
608
|
+
return null;
|
|
609
|
+
const callRe = new RegExp(`(?:^|[^\\w$.])(?:${names.map(escapeRe).join("|")})\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
|
|
610
|
+
const exprs = new Set();
|
|
611
|
+
let m;
|
|
612
|
+
while ((m = callRe.exec(text)) !== null) {
|
|
613
|
+
for (const a of splitArgWindow(m[1] || "")) {
|
|
614
|
+
const t = a.trim();
|
|
615
|
+
if (t && /^[A-Za-z_$][\w$]*(?:\s*\.\s*[\w$]+|\[[^\]]*\])*$/.test(t))
|
|
616
|
+
exprs.add(t.replace(/\s+/g, ""));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (exprs.size === 0)
|
|
620
|
+
return null;
|
|
621
|
+
// 前缀 `(?:^|[^\w$.])` 是精度关键:`name` 不能匹配 `other.name` 里的 name,
|
|
622
|
+
// 但 `args.name` 能匹配 `path.join(DIR, args.name)` 里的 args.name。
|
|
623
|
+
return new RegExp([...exprs].map((e) => `(?:^|[^\\w$.])${escapeRe(e)}\\b`).join("|"));
|
|
624
|
+
}
|
|
625
|
+
// ═══════════════════════════════════════════════════════════════
|
|
626
|
+
// C4b(2026-09-20):项目自有的「纯塑形」helper —— 补齐 C4 的最后一跳
|
|
627
|
+
//
|
|
628
|
+
// 缺口形态(fr-016 真实语料,iterateAsyncApiComponents / iterateComponents):
|
|
629
|
+
// const filename = getFileNamePath(componentDirPath, componentName, ext);
|
|
630
|
+
// writeToFileByExtension(componentData, filename); // sink 侧已继承(fr-016)
|
|
631
|
+
// // getFileNamePath(a, b, c) { return path.join(a, b) + `.${c}`; }
|
|
632
|
+
// `componentName` 已被 Object.keys 污染,但 `getFileNamePath` 不在
|
|
633
|
+
// TS_PATH_SHAPER_RE 里(那是 node:path 家族 + String 原型方法的固定清单),
|
|
634
|
+
// 于是 `filename` 收不到污点 —— 整条流断在最后一跳。
|
|
635
|
+
//
|
|
636
|
+
// 判据(三条同时成立才认,宁可漏、不可错):
|
|
637
|
+
// ① 有形参,且有带表达式的 return(void / 只写文件的函数不算)
|
|
638
|
+
// ② 函数体内没有文件 sink —— 含 sink 说明它不只是塑形
|
|
639
|
+
// ③ 每个 return 表达式里:所有调用都是塑形调用(node:path 家族 / 保值字符串
|
|
640
|
+
// 方法 / 已认定的纯塑形 helper),且所有自由标识符都是自己的形参
|
|
641
|
+
// ③ 的后半句是精度核心:return 里只要出现任何**不是形参**的自由标识符
|
|
642
|
+
// (模块常量、闭包变量、别的调用结果),就不认。
|
|
643
|
+
//
|
|
644
|
+
// 这与 C4 是同一条原则(白名单优于黑名单):只对**能证明**的形态传播。
|
|
645
|
+
// 认不出来的 helper 不传播 —— 那是可见的漏报,比不可见的误报便宜(R9)。
|
|
646
|
+
// 有界:两轮不动点,允许 helper 调用已认定的 helper,但不追环。
|
|
647
|
+
// ═══════════════════════════════════════════════════════════════
|
|
648
|
+
/** node:path 家族里只做塑形、不做校验的方法名 */
|
|
649
|
+
const PATH_SHAPER_METHODS = new Set([
|
|
650
|
+
"join", "resolve", "normalize", "basename", "dirname",
|
|
651
|
+
"extname", "relative", "format", "toNamespacedPath",
|
|
652
|
+
]);
|
|
653
|
+
/**
|
|
654
|
+
* 字符过滤方法 —— **故意不进** C4b 的证据集。
|
|
655
|
+
*
|
|
656
|
+
* C4 的内联规则里 `.replace/.trim/...` 是算塑形的(TS_PATH_SHAPER_RE 第二组),
|
|
657
|
+
* 但那是「参数窗口里看得见整个表达式」时的取舍。helper 形式看不见实参窗口,
|
|
658
|
+
* 判据就该更严:`.replace(/[^a-z0-9]/gi, "")` 恰恰是净化函数最常见的写法,
|
|
659
|
+
* 把字符过滤当成「确定不净化」的证据,正是 C4 当初用白名单避开的那类错误
|
|
660
|
+
* (`readSanitized` 用例锁的就是它)。
|
|
661
|
+
* 代价:`toSlug(p) { return p.trim().toLowerCase(); }` 仍不传播(已知缺口)。
|
|
662
|
+
*/
|
|
663
|
+
const VALUE_STRING_METHODS = new Set([
|
|
664
|
+
"trim", "trimStart", "trimEnd", "replace", "replaceAll",
|
|
665
|
+
"toLowerCase", "toUpperCase", "toString", "slice", "substring",
|
|
666
|
+
"substr", "padStart", "padEnd", "concat", "normalize",
|
|
667
|
+
]);
|
|
668
|
+
/** return 表达式里允许出现的非形参自由标识符(语言/模块级常量) */
|
|
669
|
+
const SHAPER_ALLOWED_FREE_IDENTS = new Set([
|
|
670
|
+
"path", "sep", "undefined", "null", "true", "false", "String", "Number",
|
|
671
|
+
]);
|
|
672
|
+
/** 允许出现在调用位的值转换函数(不改变路径语义) */
|
|
673
|
+
const VALUE_CONVERTER_CALLS = new Set(["String", "Number", "Boolean"]);
|
|
674
|
+
/**
|
|
675
|
+
* 判断 return 表达式是不是「纯塑形」:
|
|
676
|
+
* 所有调用都在塑形白名单内,且所有自由标识符都是形参(或模块级字面量常量)。
|
|
677
|
+
*
|
|
678
|
+
* 实现上刻意做**单趟标识符扫描**(看每个标识符的紧邻字符),而不是
|
|
679
|
+
* `NAME(?!\s*\()` 这种前瞻 —— 前瞻会被正则回溯绕过:
|
|
680
|
+
* 对 `withExt(...)`,`([A-Za-z_$][\w$]*)(?!\s*\()` 可以退化成匹配 `withEx`
|
|
681
|
+
* 让前瞻通过,于是把调用名误判成自由标识符(2026-09-20 实测踩到)。
|
|
682
|
+
*/
|
|
683
|
+
function isPureShaperExpr(expr, params, shapers, extraIdents) {
|
|
684
|
+
// 先抹掉字符串与模板字面量:里面的 `${...}` 可能带花括号,会干扰后续扫描;
|
|
685
|
+
// 抹掉后如果表达式里还有别的自由标识符,仍会被下面的检查拦住。
|
|
686
|
+
const s = expr
|
|
687
|
+
.replace(/`(?:\\.|\$\{[^}]*\}|[^`\\])*`/g, '""')
|
|
688
|
+
.replace(/'(?:\\.|[^'\\])*'/g, '""')
|
|
689
|
+
.replace(/"(?:\\.|[^"\\])*"/g, '""');
|
|
690
|
+
const idRe = /[A-Za-z_$][\w$]*/g;
|
|
691
|
+
let m;
|
|
692
|
+
while ((m = idRe.exec(s)) !== null) {
|
|
693
|
+
const n = m[0];
|
|
694
|
+
// 紧前一个非空字符 / 紧后一个非空字符
|
|
695
|
+
let i = m.index - 1;
|
|
696
|
+
while (i >= 0 && /\s/.test(s[i]))
|
|
697
|
+
i--;
|
|
698
|
+
const before = i >= 0 ? s[i] : "";
|
|
699
|
+
let j = m.index + n.length;
|
|
700
|
+
while (j < s.length && /\s/.test(s[j]))
|
|
701
|
+
j++;
|
|
702
|
+
const after = j < s.length ? s[j] : "";
|
|
703
|
+
if (before === ".")
|
|
704
|
+
continue; // 成员名(`.join` / `.name`),不参与判定
|
|
705
|
+
if (after === "(") {
|
|
706
|
+
// 调用位:必须落在塑形白名单。字符过滤方法(`VALUE_STRING_METHODS`)
|
|
707
|
+
// **不算**证据 —— 见其定义处注释。
|
|
708
|
+
if (!PATH_SHAPER_METHODS.has(n) && !VALUE_CONVERTER_CALLS.has(n) && !shapers.has(n)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
// 自由标识符:必须是自己的形参、语言级常量白名单,或**已确证的字面量常量 / path 别名**
|
|
714
|
+
if (params.has(n) || SHAPER_ALLOWED_FREE_IDENTS.has(n))
|
|
715
|
+
continue;
|
|
716
|
+
if (extraIdents && extraIdents.has(n))
|
|
717
|
+
continue;
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
/** 收集模块级(top-level)**字面量**常量名 —— helper 的 return 引用它们仍是纯塑形 */
|
|
723
|
+
function moduleLiteralConstNames(project) {
|
|
724
|
+
const out = new Set();
|
|
725
|
+
for (const sf of project.getSourceFiles()) {
|
|
726
|
+
for (const vd of sf.getVariableDeclarations()) {
|
|
727
|
+
// 只要模块作用域:VariableStatement 的父节点必须是 SourceFile
|
|
728
|
+
const stmt = vd.getVariableStatement();
|
|
729
|
+
if (!stmt || !ts_morph_1.Node.isSourceFile(stmt.getParent()))
|
|
730
|
+
continue;
|
|
731
|
+
const init = vd.getInitializer();
|
|
732
|
+
if (!init)
|
|
733
|
+
continue;
|
|
734
|
+
// 只收「字面量」—— 一旦允许任意表达式,helper 就能把别处的污点藏在常量后面
|
|
735
|
+
// (C4e 政策:看不见的一律不传播)
|
|
736
|
+
const isLiteral = ts_morph_1.Node.isStringLiteral(init) ||
|
|
737
|
+
ts_morph_1.Node.isNoSubstitutionTemplateLiteral(init) ||
|
|
738
|
+
ts_morph_1.Node.isNumericLiteral(init) ||
|
|
739
|
+
init.getKind() === ts.SyntaxKind.TrueKeyword ||
|
|
740
|
+
init.getKind() === ts.SyntaxKind.FalseKeyword;
|
|
741
|
+
if (isLiteral)
|
|
742
|
+
out.add(vd.getName());
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return out;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* C4e:`import * as p from "path"` / `import nodePath from "node:path"` /
|
|
749
|
+
* `import { join } from "path"` —— 这些局部名随项目各异,但**出处可确证**是
|
|
750
|
+
* path 模块,helper 的 return 引用它们仍是纯塑形。
|
|
751
|
+
* 只放行 path 模块本身(含 node: 前缀),不放行 fs/os —— 后者会被 helper 用来把
|
|
752
|
+
* IO 藏进 return(判据②只扫 sink 调用,覆盖不到「返回内嵌读写」这一类)。
|
|
753
|
+
*/
|
|
754
|
+
function pathModuleAliases(project) {
|
|
755
|
+
const out = new Set(["path"]);
|
|
756
|
+
for (const sf of project.getSourceFiles()) {
|
|
757
|
+
for (const id of sf.getImportDeclarations()) {
|
|
758
|
+
const spec = id.getModuleSpecifierValue();
|
|
759
|
+
if (spec !== "path" && spec !== "node:path")
|
|
760
|
+
continue;
|
|
761
|
+
// 实测(ts-morph 本仓库版本):NamespaceImport 上既无 getName() 也无
|
|
762
|
+
// getNameNode(),只有 getText() —— 返回的就是别名本身。
|
|
763
|
+
const ns = id.getNamespaceImport(); // `import * as X`
|
|
764
|
+
if (ns)
|
|
765
|
+
out.add(ns.getText().replace(/[^\w$]/g, ""));
|
|
766
|
+
const def = id.getDefaultImport();
|
|
767
|
+
if (def)
|
|
768
|
+
out.add(def.getText());
|
|
769
|
+
for (const nb of id.getNamedImports())
|
|
770
|
+
out.add(nb.getName());
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return out;
|
|
774
|
+
}
|
|
775
|
+
/** return 表达式列表:块体取 return 语句,箭头简洁体把整个体当作返回值 */
|
|
776
|
+
function returnExprsOf(body) {
|
|
777
|
+
if (!ts_morph_1.Node.isBlock(body))
|
|
778
|
+
return [body.getText()]; // 箭头简洁体
|
|
779
|
+
const rets = [];
|
|
780
|
+
for (const rs of body.getDescendantsOfKind(ts.SyntaxKind.ReturnStatement)) {
|
|
781
|
+
const e = rs.getExpression();
|
|
782
|
+
if (e)
|
|
783
|
+
rets.push(e.getText());
|
|
784
|
+
}
|
|
785
|
+
return rets;
|
|
786
|
+
}
|
|
787
|
+
/** 扫描全项目,收集「纯塑形」helper 的名字(有界两轮不动点) */
|
|
788
|
+
function pureShaperFunctionNames(project) {
|
|
789
|
+
const sinkRe = tsSinkCallRegex();
|
|
790
|
+
const cands = [];
|
|
791
|
+
/** 登记一个候选:三条判据 —— 有形参 / 有返回值 / 体内无 sink */
|
|
792
|
+
const consider = (name, paramNodes, declText, bodyNode) => {
|
|
793
|
+
const params = new Set(paramNodes.filter(Boolean));
|
|
794
|
+
if (params.size === 0)
|
|
795
|
+
return;
|
|
796
|
+
sinkRe.lastIndex = 0;
|
|
797
|
+
if (sinkRe.test(declText))
|
|
798
|
+
return; // ②
|
|
799
|
+
// 用 AST 取 return 表达式,而不是正则 —— 模板字面量里的 `${}` 会
|
|
800
|
+
// 让「遇到花括号就停」的朴素正则在 `path.join(a,b) + \`.${c}\`` 上截断。
|
|
801
|
+
const rets = returnExprsOf(bodyNode);
|
|
802
|
+
if (rets.length === 0)
|
|
803
|
+
return; // ①
|
|
804
|
+
cands.push({ name, params, rets });
|
|
805
|
+
};
|
|
806
|
+
for (const sf of project.getSourceFiles()) {
|
|
807
|
+
// (a) 函数声明
|
|
808
|
+
for (const fn of sf.getFunctions()) {
|
|
809
|
+
const name = fn.getName();
|
|
810
|
+
if (!name)
|
|
811
|
+
continue;
|
|
812
|
+
const body = fn.getBody();
|
|
813
|
+
if (!body)
|
|
814
|
+
continue; // 重载签名 / declare 只有签名
|
|
815
|
+
consider(name, fn.getParameters().map((p) => p.getName()), fn.getText(), body);
|
|
816
|
+
}
|
|
817
|
+
// (b) 变量承载的箭头 / 函数表达式 —— 现代 TS 里 helper 的主力写法
|
|
818
|
+
// `export const toPath = (n: string) => n + ".md";`
|
|
819
|
+
// sf.getFunctions() 收不到这些,2026-09-20 实测整片漏。
|
|
820
|
+
for (const vd of sf.getVariableDeclarations()) {
|
|
821
|
+
const init = vd.getInitializer();
|
|
822
|
+
if (!init)
|
|
823
|
+
continue;
|
|
824
|
+
const isArrow = ts_morph_1.Node.isArrowFunction(init);
|
|
825
|
+
const isFnExpr = ts_morph_1.Node.isFunctionExpression(init);
|
|
826
|
+
if (!isArrow && !isFnExpr)
|
|
827
|
+
continue;
|
|
828
|
+
const fnLike = init;
|
|
829
|
+
const body = fnLike.getBody();
|
|
830
|
+
if (!body)
|
|
831
|
+
continue;
|
|
832
|
+
consider(vd.getName(), fnLike.getParameters().map((p) => p.getName()), init.getText(), body);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// 形参之外允许出现的自由标识符 = 模块级字面量常量 ∪ 确证的 path 模块别名
|
|
836
|
+
const extraIdents = moduleLiteralConstNames(project);
|
|
837
|
+
for (const a of pathModuleAliases(project))
|
|
838
|
+
extraIdents.add(a);
|
|
839
|
+
const shapers = new Set();
|
|
840
|
+
for (let round = 0; round < 2; round++) {
|
|
841
|
+
let added = false;
|
|
842
|
+
for (const c of cands) {
|
|
843
|
+
if (shapers.has(c.name))
|
|
844
|
+
continue;
|
|
845
|
+
const ok = c.rets.every((e) => isPureShaperExpr(e, c.params, shapers, extraIdents));
|
|
846
|
+
if (!ok)
|
|
847
|
+
continue;
|
|
848
|
+
shapers.add(c.name);
|
|
849
|
+
added = true;
|
|
850
|
+
}
|
|
851
|
+
if (!added)
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
return shapers;
|
|
571
855
|
}
|
|
572
856
|
const TS_FILE_SINK_NAMES = [
|
|
573
857
|
"readFile", "readFileSync", "writeFile", "writeFileSync",
|
|
@@ -600,6 +884,29 @@ const UNTRUSTED_ROOTS = [
|
|
|
600
884
|
expr: String.raw `\bparams\.arguments\b[.[]?`,
|
|
601
885
|
why: "MCP CallToolRequest.params.arguments —— 工具调用实参,调用方可控(fr-012)",
|
|
602
886
|
},
|
|
887
|
+
{
|
|
888
|
+
id: "document_parse",
|
|
889
|
+
expr: String.raw `\b(?:JSON|JSON5)\s*\.\s*parse\b|\b(?:YAML|yaml)\s*\.\s*(?:parse|load)\b|\b(?:parse|load|read)(?:Yaml|YAML|Json|JSON|Toml|TOML|Xml|XML|Csv|CSV)\s*\(`,
|
|
890
|
+
why: "外部文档的解析产物:值的**内容**由被解析的输入决定,而非由代码写死(fr-016 的 OpenAPI/AsyncAPI 描述文件经 parseYaml 进入后再 split 落盘)",
|
|
891
|
+
},
|
|
892
|
+
// ── 一类性质不同的根(务必读懂再改)──
|
|
893
|
+
// 上面两条是**传输面**:值跨进程边界进来,来源性质无歧义。这一条不是。
|
|
894
|
+
// `Object.keys/values/entries` 是语言运算,声明的是一件更弱的事:
|
|
895
|
+
// 「这些名字/值不是字面量,而是运行时数据结构的产物」。
|
|
896
|
+
// 之所以仍然列为根,理由与取舍:
|
|
897
|
+
// - 根的职责是【如实描述值的来源】,不是做是否有害的价值判断。
|
|
898
|
+
// 键名能否落到 fs sink、以及有没有被校验,那是前缀修剪与守卫证据的事
|
|
899
|
+
// (G1/G2)。压掉来源的代价是不可见的漏报,而留下来的误报是可见的。
|
|
900
|
+
// - 它是 fr-016 唯一本地可观测的入口:文档本体已由**上游**解析好作为形参
|
|
901
|
+
// 传入(`channels: Record<string, any>`),函数体内没有任何解析调用,
|
|
902
|
+
// 看得见的只有这次枚举。
|
|
903
|
+
// 代价:键名来自内部固定字典(如 `for (const l of Object.keys(locales))`)
|
|
904
|
+
// 也会被标记。若工具体现为噪声过大,应优先收窄**这里**,不要在守卫侧放宽。
|
|
905
|
+
{
|
|
906
|
+
id: "runtime_key_enum",
|
|
907
|
+
expr: String.raw `\bObject\s*\.\s*(?:keys|values|entries)\s*\(`,
|
|
908
|
+
why: "运行时枚举产物:键名/键值由数据结构内容决定,不是字面量(fr-016 的 Object.keys(channels) → channelName)",
|
|
909
|
+
},
|
|
603
910
|
];
|
|
604
911
|
/** 所有根的来源片段,供 **无标志** 的布尔探测正则使用 */
|
|
605
912
|
const UNTRUSTED_ROOT_SRC = UNTRUSTED_ROOTS.map((r) => `(?:${r.expr})`).join("|");
|
|
@@ -633,7 +940,7 @@ const TS_PATH_SHAPER_RE = /(?:path\s*\.\s*(?:posix|win32)\s*\.\s*|path\s*\.\s*)?
|
|
|
633
940
|
/** 拼接形态:`+` 或反引号(模板字面量)—— 同样只是塑形 */
|
|
634
941
|
const TS_CONCAT_RE = /\+|`/;
|
|
635
942
|
/** 函数体内被不可信根污染的局部名(含解构、单跳赋值与 C4 塑形传播,深度 ≤3) */
|
|
636
|
-
function collectTaintedNames(text) {
|
|
943
|
+
function collectTaintedNames(text, shapers) {
|
|
637
944
|
const tainted = new Set();
|
|
638
945
|
let m;
|
|
639
946
|
const root = UNTRUSTED_ROOT_SRC;
|
|
@@ -651,21 +958,108 @@ function collectTaintedNames(text) {
|
|
|
651
958
|
const bareAssign = new RegExp(`(?:^|[^\\w$.])([\\w$]+)\\s*=\\s*(?:await\\s+)?(?:${root})`, "g");
|
|
652
959
|
while ((m = bareAssign.exec(text)) !== null)
|
|
653
960
|
tainted.add(m[1]);
|
|
654
|
-
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
let
|
|
662
|
-
while ((
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
961
|
+
// 枚举绑定:`for (const k of Object.keys(doc))` / `for (const [k, v] of Object.entries(doc))`
|
|
962
|
+
// fr-016 的污点就是这样进来的 —— 文档本体已由上游解析好作为形参传入,
|
|
963
|
+
// 函数体内没有赋值语句,只有这次枚举。少了这条,iterate-* 系列函数的
|
|
964
|
+
// 局部变量一个都收不到(此前 MISS 的一半原因在此)。
|
|
965
|
+
const bindFromEnum = (pattern) => {
|
|
966
|
+
const enumBind = new RegExp(`for\\s*\\(\\s*(?:const|let|var)\\s+([\\w$\\[\\],\\s]+?)\\s+(?:of|in)\\s+(?:await\\s+)?(?:${pattern})`, "g");
|
|
967
|
+
let hit = false;
|
|
968
|
+
let em;
|
|
969
|
+
while ((em = enumBind.exec(text)) !== null) {
|
|
970
|
+
const binding = em[1].replace(/[[\]]/g, " ");
|
|
971
|
+
for (const part of binding.split(",")) {
|
|
972
|
+
const name = part.trim().split(":")[0].trim();
|
|
973
|
+
if (/^[\w$]+$/.test(name) && !tainted.has(name)) {
|
|
974
|
+
tainted.add(name);
|
|
975
|
+
hit = true;
|
|
976
|
+
}
|
|
666
977
|
}
|
|
667
978
|
}
|
|
668
|
-
|
|
979
|
+
return hit;
|
|
980
|
+
};
|
|
981
|
+
bindFromEnum(root);
|
|
982
|
+
// ── C4d(2026-09-20):高阶枚举方法的回调形参 ──
|
|
983
|
+
// `Object.keys(doc).forEach(k => …)` / `ks.map(k => …)` 与 for-of 语义等价
|
|
984
|
+
// (形参 = 被枚举到的元素),但此前只绑 for-of/for-in,这类整片漏。
|
|
985
|
+
// 实测(2026-09-20 探针):Object.keys/entries 后接 forEach/map 的 4 种
|
|
986
|
+
// 写法全部 MISS,而 for-in 其实一直是通的 —— 缺口在回调,不在 for-in。
|
|
987
|
+
//
|
|
988
|
+
// 方法表的取舍(务必读懂再扩):只收【枚举全部元素、且形参 = 元素本身】的
|
|
989
|
+
// forEach/map/flatMap/filter 四个。不收:
|
|
990
|
+
// - find/some/every —— 谓词,形参虽是元素但语义是「判定」,且常配合
|
|
991
|
+
// 白名单做守卫,盲目绑进去反而给守卫侧喂误报;
|
|
992
|
+
// - reduce —— 第一个形参是累加器不是元素,语义错位。
|
|
993
|
+
// 认不出就不传播,代价是漏报,不是误报。
|
|
994
|
+
//
|
|
995
|
+
// ── C4d-b(2026-09-20,同日第二次修正)──
|
|
996
|
+
// 初版正则以 `[,)]` 收尾,带来两件事:
|
|
997
|
+
// ① `forEach(handleOne)` —— 回调是【函数引用】而非内联函数,`handleOne` 被
|
|
998
|
+
// 当成形参绑进污点集合。探针 `Object.keys(doc).forEach(handle); … = handle`
|
|
999
|
+
// 实测误标:这是把回调名当元素,语义错误;
|
|
1000
|
+
// ② 只认 `(k) =>` 带括号的箭头,`k =>` / `function (k) {}` 两种常见写法漏。
|
|
1001
|
+
// 改法:分两条 —— 箭头分支形参后必须见 `=>`(括号可有可无),ES5 分支必须见
|
|
1002
|
+
// `function` 关键字。两者都要求【回调体是内联的】,函数引用形态自然被排除。
|
|
1003
|
+
const CALLBACK_ENUM_METHODS = "forEach|map|flatMap|filter";
|
|
1004
|
+
const bindFromCallback = (receiver) => {
|
|
1005
|
+
const callbackRe = (paramsTail) => new RegExp(`${receiver}\\s*\\.\\s*(?:${CALLBACK_ENUM_METHODS})\\s*\\(\\s*(?:async\\s+)?${paramsTail}`, "g");
|
|
1006
|
+
// 形参字符集要容得下 TS 的**类型标注**(`(s: any) =>` 是 TS 工程里的常态,
|
|
1007
|
+
// 漏了 `:` 会让整环不匹配 —— 2026-09-20 实测踩到),因此放行 `:.|<>`
|
|
1008
|
+
// (联合类型与泛型形参);`=>` 与 `{` 仍不在集合内,函数体不会被吃进来。
|
|
1009
|
+
const params = String.raw `\(?\s*([\w$\[\],.:\s<>|]+?)\s*\)?`;
|
|
1010
|
+
// 箭头回调:`(k) =>` / `k =>` / `async (k) =>` / `([k, v]) =>`
|
|
1011
|
+
const arrow = callbackRe(`${params}\\s*=>`);
|
|
1012
|
+
// ES5 回调:`function (k) { … }` —— 不是箭头,但形参同样是元素
|
|
1013
|
+
const fnCb = callbackRe(String.raw `function\s*\(?\s*([\w$\[\],.:\s<>|]+?)\s*\)`);
|
|
1014
|
+
let hit = false;
|
|
1015
|
+
for (const re of [arrow, fnCb]) {
|
|
1016
|
+
let cm;
|
|
1017
|
+
while ((cm = re.exec(text)) !== null) {
|
|
1018
|
+
const raw = cm[1].trim();
|
|
1019
|
+
// `[k, v]` 解构:两个都是元素,都绑;`k, i` 只取第一个(第二个是索引)
|
|
1020
|
+
const parts = raw.startsWith("[")
|
|
1021
|
+
? raw.replace(/[[\]]/g, " ").split(",")
|
|
1022
|
+
: [raw.split(",")[0]];
|
|
1023
|
+
for (const part of parts) {
|
|
1024
|
+
const name = part.trim().split(":")[0].trim();
|
|
1025
|
+
if (/^[\w$]+$/.test(name) && !tainted.has(name)) {
|
|
1026
|
+
tainted.add(name);
|
|
1027
|
+
hit = true;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return hit;
|
|
1033
|
+
};
|
|
1034
|
+
// 根形态:`Object.keys(doc).forEach(...)` —— root 正则只吃到开括号,
|
|
1035
|
+
// 这里补 `[^()]*\)` 吃掉实参列表与闭括号(`Object.keys` 的实参无嵌套)。
|
|
1036
|
+
bindFromCallback(`(?:${root})[^()]*\\)`);
|
|
1037
|
+
// ── 有界不动点(≤3 轮):塑形传播 / 聚合迭代 / 单跳赋值 ──
|
|
1038
|
+
// C4c:三轮里的第二轮才是关键 —— `const ks = Object.keys(o); for (const k of ks)`
|
|
1039
|
+
// 的被迭代对象是**已被污染的变量**,不再是根表达式。此前 enumBind 只按 root
|
|
1040
|
+
// 匹配,这种「先收集再迭代」的写法一根都收不到(fr-016 的 gather* 系列如此)。
|
|
1041
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
1042
|
+
let grew = taintedViaShaper(text, tainted, shapers);
|
|
1043
|
+
if (tainted.size > 0) {
|
|
1044
|
+
const names = [...tainted].map(escapeRe).join("|");
|
|
1045
|
+
if (bindFromEnum(`\\b(?:${names})\\b`))
|
|
1046
|
+
grew = true;
|
|
1047
|
+
// C4d 变量形态:`const ks = Object.keys(doc); ks.forEach(k => …)`
|
|
1048
|
+
// C4d-b:接收者允许中间隔着成员 —— `doc.sections.forEach((s) => …)`。
|
|
1049
|
+
// doc/ks 已污时,它的成员同样是污点数据的组成部分,枚举出来的元素也是。
|
|
1050
|
+
// 末尾仍必须是四个枚举方法之一,`doc.length` 之类不会触发。
|
|
1051
|
+
if (bindFromCallback(`(?:^|[^\\w$.])(?:${names})(?:\\s*\\.\\s*[\\w$]+)*`))
|
|
1052
|
+
grew = true;
|
|
1053
|
+
const hop = new RegExp(`(?:const|let|var)\\s+([\\w$]+)\\s*=\\s*\\b(?:${names})\\b`, "g");
|
|
1054
|
+
let hm;
|
|
1055
|
+
while ((hm = hop.exec(text)) !== null) {
|
|
1056
|
+
if (!tainted.has(hm[1])) {
|
|
1057
|
+
tainted.add(hm[1]);
|
|
1058
|
+
grew = true;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
if (!grew)
|
|
669
1063
|
break;
|
|
670
1064
|
}
|
|
671
1065
|
return tainted;
|
|
@@ -678,7 +1072,13 @@ function collectTaintedNames(text) {
|
|
|
678
1072
|
* `normalized = path.normalize(p)` → `savePath = path.join(normalized, f)`
|
|
679
1073
|
* 这类链式构造能接上(fr-012 下载侧守卫块就是这个形状)。
|
|
680
1074
|
*/
|
|
681
|
-
function taintedViaShaper(text, tainted) {
|
|
1075
|
+
function taintedViaShaper(text, tainted, shapers) {
|
|
1076
|
+
// C4b:项目自有「纯塑形」helper 的调用也算子(如 getFileNamePath(dir, name, ext))。
|
|
1077
|
+
// 见 pureShaperFunctionNames —— 只认三条判据全部成立的函数,认不出就不传播。
|
|
1078
|
+
const localShaperRe = shapers && shapers.size > 0
|
|
1079
|
+
? new RegExp(`(?:^|[^\\w$.])(?:${[...shapers].map(escapeRe).join("|")})\\s*\\(`)
|
|
1080
|
+
: null;
|
|
1081
|
+
let grewAny = false;
|
|
682
1082
|
for (let depth = 0; depth < 3; depth++) {
|
|
683
1083
|
// 注意:这里**不能**以 `tainted.size === 0` 提前退出 —— 种子是根模式
|
|
684
1084
|
// (taintPattern 恒定并入 UNTRUSTED_ROOT_SRC)。本轮这 4 条 known-gap
|
|
@@ -686,7 +1086,7 @@ function taintedViaShaper(text, tainted) {
|
|
|
686
1086
|
// 加这道闸门等于整条 C4 不生效(2026-09-19 实测踩到)。
|
|
687
1087
|
const taintRe = taintPattern(tainted);
|
|
688
1088
|
if (!taintRe)
|
|
689
|
-
return;
|
|
1089
|
+
return grewAny;
|
|
690
1090
|
let added = false;
|
|
691
1091
|
for (const line of text.split("\n")) {
|
|
692
1092
|
const m = /^\s*(?:const|let|var)?\s*([\w$]+)\s*=\s*(?:await\s+)?(.+)$/.exec(line);
|
|
@@ -696,14 +1096,19 @@ function taintedViaShaper(text, tainted) {
|
|
|
696
1096
|
const rhs = m[2];
|
|
697
1097
|
if (tainted.has(name))
|
|
698
1098
|
continue;
|
|
699
|
-
|
|
1099
|
+
const shaped = TS_PATH_SHAPER_RE.test(rhs) ||
|
|
1100
|
+
TS_CONCAT_RE.test(rhs) ||
|
|
1101
|
+
(localShaperRe !== null && localShaperRe.test(rhs));
|
|
1102
|
+
if (taintRe.test(rhs) && shaped) {
|
|
700
1103
|
tainted.add(name);
|
|
701
1104
|
added = true;
|
|
702
1105
|
}
|
|
703
1106
|
}
|
|
704
1107
|
if (!added)
|
|
705
|
-
return;
|
|
1108
|
+
return grewAny;
|
|
1109
|
+
grewAny = true;
|
|
706
1110
|
}
|
|
1111
|
+
return grewAny;
|
|
707
1112
|
}
|
|
708
1113
|
function taintPattern(tainted) {
|
|
709
1114
|
const parts = [UNTRUSTED_ROOT_SRC];
|
|
@@ -712,16 +1117,33 @@ function taintPattern(tainted) {
|
|
|
712
1117
|
}
|
|
713
1118
|
return new RegExp(parts.join("|"));
|
|
714
1119
|
}
|
|
715
|
-
/**
|
|
716
|
-
|
|
1120
|
+
/**
|
|
1121
|
+
* 本函数体内:文件 sink 的实参窗口含 request 污点 → true。
|
|
1122
|
+
*
|
|
1123
|
+
* `sanitizedRe` 非空时(G2):实参里被净化过的表达式不再算污点 ——
|
|
1124
|
+
* 逐实参判定,只要还有一个实参含【未被净化】的污点就照旧标记。
|
|
1125
|
+
*/
|
|
1126
|
+
function hasTaintedSinkCall(text, tainted, sanitizedRe) {
|
|
717
1127
|
const taint = taintPattern(tainted);
|
|
718
1128
|
if (!taint)
|
|
719
1129
|
return false;
|
|
720
1130
|
const sinkRe = tsSinkCallRegex();
|
|
721
1131
|
let m;
|
|
722
1132
|
while ((m = sinkRe.exec(text)) !== null) {
|
|
723
|
-
|
|
724
|
-
|
|
1133
|
+
const win = m[2] || "";
|
|
1134
|
+
if (!taint.test(win))
|
|
1135
|
+
continue;
|
|
1136
|
+
if (sanitizedRe) {
|
|
1137
|
+
const args = splitArgWindow(win);
|
|
1138
|
+
if (args.length > 0) {
|
|
1139
|
+
if (!args.some((a) => taint.test(a) && !sanitizedRe.test(a)))
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
else if (sanitizedRe.test(win)) {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return true;
|
|
725
1147
|
}
|
|
726
1148
|
return false;
|
|
727
1149
|
}
|
|
@@ -749,6 +1171,7 @@ function splitArgWindow(win) {
|
|
|
749
1171
|
/** 项目方法名 → 方法体内流入文件 sink 的形参下标集合 */
|
|
750
1172
|
function methodSinkParamMap(project, absRoot) {
|
|
751
1173
|
const map = new Map();
|
|
1174
|
+
const bodies = [];
|
|
752
1175
|
const register = (keyName, fullName, text, relPath, params) => {
|
|
753
1176
|
const sinkRe = tsSinkCallRegex();
|
|
754
1177
|
let sm;
|
|
@@ -780,7 +1203,9 @@ function methodSinkParamMap(project, absRoot) {
|
|
|
780
1203
|
const params = m.getParameters().map((p) => p.getName());
|
|
781
1204
|
if (params.length === 0)
|
|
782
1205
|
continue;
|
|
783
|
-
|
|
1206
|
+
const body = { keyName: m.getName(), fullName: `${cn}.${m.getName()}`, relPath, params, text: m.getText() };
|
|
1207
|
+
bodies.push(body);
|
|
1208
|
+
register(body.keyName, body.fullName, body.text, relPath, params);
|
|
784
1209
|
}
|
|
785
1210
|
}
|
|
786
1211
|
// ── 污点数据流试点(2026-09-18)──
|
|
@@ -795,9 +1220,64 @@ function methodSinkParamMap(project, absRoot) {
|
|
|
795
1220
|
const params = fn.getParameters().map((p) => p.getName());
|
|
796
1221
|
if (params.length === 0)
|
|
797
1222
|
continue;
|
|
798
|
-
|
|
1223
|
+
const body = { keyName: fnName, fullName: fnName, relPath, params, text: fn.getText() };
|
|
1224
|
+
bodies.push(body);
|
|
1225
|
+
register(body.keyName, body.fullName, body.text, relPath, params);
|
|
799
1226
|
}
|
|
800
1227
|
}
|
|
1228
|
+
// ── sink 形参继承(2026-09-19,fr-016)──
|
|
1229
|
+
// 真实工程落盘几乎必过一层自有封装,只做「形参 → 本函数体内 fs sink」的
|
|
1230
|
+
// 一跳登记,等于把整层 wrapper 排除在外:
|
|
1231
|
+
// writeToFileByExtension(data, filePath) → writeJson/writeYaml(d, filePath) → fs.writeFileSync(filePath)
|
|
1232
|
+
// 于是即使上游 taint 已识别,`writeToFileByExtension(data, taintedPath)`
|
|
1233
|
+
// 也查不到任何记录 —— fr-016 的流量断了 sink 侧这一环,与来源侧的根缺口
|
|
1234
|
+
// 是**两个独立缺陷**,必须两个都补才能让真实语料动起来。
|
|
1235
|
+
//
|
|
1236
|
+
// 规矩与既有 register 完全一致、只是再推一层:自己的形参被传进已登记函数的
|
|
1237
|
+
// sink 位置 ⇒ 继承该 sink 位置。有界迭代(≤3 轮)保证收敛。
|
|
1238
|
+
//
|
|
1239
|
+
// 注意这不是 C4b。C4b 是**值侧**( helper 会不会改变污点性质);
|
|
1240
|
+
// 这里是** sink 侧**( wrapper 会不会真的写到文件)。两侧对称,缺一不可。
|
|
1241
|
+
// 注意用 new RegExp(String) 而非正则字面量:本文件同类表达式(directCallRe 等)
|
|
1242
|
+
// 都按这套写法,写成字面量时 `\\w` 会被当成「反斜杠 + w」,一个调用都匹配不到。
|
|
1243
|
+
const wrapperCallRe = new RegExp(`(?:^|[^\\w$.])([\\w$]+)\\s*\\(([\\s\\S]{0,250}?)\\)`, "g");
|
|
1244
|
+
for (let round = 0; round < 3; round++) {
|
|
1245
|
+
let grew = false;
|
|
1246
|
+
for (const b of bodies) {
|
|
1247
|
+
wrapperCallRe.lastIndex = 0;
|
|
1248
|
+
let cm;
|
|
1249
|
+
while ((cm = wrapperCallRe.exec(b.text)) !== null) {
|
|
1250
|
+
const callee = map.get(cm[1]);
|
|
1251
|
+
if (!callee || callee.idxs.size === 0)
|
|
1252
|
+
continue;
|
|
1253
|
+
const args = splitArgWindow(cm[2] || "");
|
|
1254
|
+
if (args.length === 0)
|
|
1255
|
+
continue;
|
|
1256
|
+
b.params.forEach((pname, idx) => {
|
|
1257
|
+
if (!pname)
|
|
1258
|
+
return;
|
|
1259
|
+
const pre = new RegExp(`\\b${escapeRe(pname)}\\b`);
|
|
1260
|
+
// 形参出现在被调用方的【sink 形参位】上,才算继承了 sink
|
|
1261
|
+
const inherited = [...callee.idxs].some((i) => i < args.length && pre.test(args[i]));
|
|
1262
|
+
if (!inherited)
|
|
1263
|
+
return;
|
|
1264
|
+
if (!map.has(b.keyName)) {
|
|
1265
|
+
map.set(b.keyName, { idxs: new Set(), entries: [] });
|
|
1266
|
+
}
|
|
1267
|
+
const rec = map.get(b.keyName);
|
|
1268
|
+
if (rec.idxs.has(idx))
|
|
1269
|
+
return;
|
|
1270
|
+
rec.idxs.add(idx);
|
|
1271
|
+
grew = true;
|
|
1272
|
+
if (!rec.entries.some((e) => e.name === b.fullName)) {
|
|
1273
|
+
rec.entries.push({ name: b.fullName, file: b.relPath });
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (!grew)
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
801
1281
|
return map;
|
|
802
1282
|
}
|
|
803
1283
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -833,15 +1313,19 @@ function collectUrlParamNames(params) {
|
|
|
833
1313
|
* 项目方法体内的文件 sink(跨函数一跳,onMethodHit 回调登记方法条目)
|
|
834
1314
|
* - SSRF:URL 形参/request 污点 → HTTP fetch sink,无守卫词汇
|
|
835
1315
|
*/
|
|
836
|
-
function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns) {
|
|
1316
|
+
function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns, directGuardFns, shapers) {
|
|
837
1317
|
const markers = [];
|
|
838
1318
|
// ── 路径穿越 ──
|
|
839
1319
|
// G1(2026-09-19):有流还不够,必须【没有校验证据】才标记——
|
|
840
1320
|
// 与 SSRF 侧「无 SSRF_GUARD_EVIDENCE 才标记」对齐。
|
|
841
1321
|
// 未传 guardFns(旧调用点/单测)时按「不做校验识别」的旧语义处理。
|
|
842
1322
|
if (hasRequestRootedExpr(text)) {
|
|
843
|
-
const tainted = collectTaintedNames(text);
|
|
1323
|
+
const tainted = collectTaintedNames(text, shapers);
|
|
844
1324
|
const selfGuarded = guardFns ? hasPathGuardEvidence(text) !== null : false;
|
|
1325
|
+
// G2:调用点抑制 —— 被传进「自身含校验证据的函数」的表达式视为已净化。
|
|
1326
|
+
// 与 selfGuarded(函数级、按词形)互补:这条按【被调用方的实际证据】定案,
|
|
1327
|
+
// 因此认得出 assertTemplateName 这类不含路径语义后缀的自定义校验函数。
|
|
1328
|
+
const sanitizedRe = collectSanitizedExprs(text, directGuardFns);
|
|
845
1329
|
// C5(2026-09-19):不可信根可以直接写在 sink 实参里,不需要中间变量。
|
|
846
1330
|
// 此前外层要求 collectTaintedNames 非空,于是
|
|
847
1331
|
// `fs.readFileSync("/data/" + req.params.name)` —— 真实工程里最常见的形态
|
|
@@ -849,7 +1333,7 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns)
|
|
|
849
1333
|
// SSRF 侧从来不是这样(它把 UNTRUSTED_ROOT_SRC 并进 taint 模式去匹配实参窗口),
|
|
850
1334
|
// 这是同一条数据流上的又一处口径不一致。
|
|
851
1335
|
if (!selfGuarded) {
|
|
852
|
-
if (hasTaintedSinkCall(text, tainted)) {
|
|
1336
|
+
if (hasTaintedSinkCall(text, tainted, sanitizedRe)) {
|
|
853
1337
|
markers.push("__progmune_path_traversal__");
|
|
854
1338
|
}
|
|
855
1339
|
else {
|
|
@@ -872,7 +1356,7 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns)
|
|
|
872
1356
|
if (guardFns && guardFns.has(m[1]))
|
|
873
1357
|
continue;
|
|
874
1358
|
const args = splitArgWindow(m[2] || "");
|
|
875
|
-
const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg));
|
|
1359
|
+
const hit = args.some((arg, i) => rec.idxs.has(i) && taint.test(arg) && !(sanitizedRe && sanitizedRe.test(arg)));
|
|
876
1360
|
if (hit) {
|
|
877
1361
|
markers.push("__progmune_path_traversal__");
|
|
878
1362
|
onMethodHit(rec);
|
|
@@ -887,7 +1371,7 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns)
|
|
|
887
1371
|
TS_HTTP_FETCH_SINK.lastIndex = 0;
|
|
888
1372
|
if (TS_HTTP_FETCH_SINK.test(text) && !SSRF_GUARD_EVIDENCE.test(text)) {
|
|
889
1373
|
const urlParams = paramNames.filter((n) => URL_PARAM_NAME.test(n));
|
|
890
|
-
const reqTainted = collectTaintedNames(text);
|
|
1374
|
+
const reqTainted = collectTaintedNames(text, shapers);
|
|
891
1375
|
const taintParts = [];
|
|
892
1376
|
if (urlParams.length > 0)
|
|
893
1377
|
taintParts.push(`\\b(?:${urlParams.join("|")})\\b`);
|
|
@@ -990,7 +1474,14 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
990
1474
|
// pendingMethodMarks = 跨函数命中待标记的方法条目(主循环后统一应用)
|
|
991
1475
|
const sinkParams = methodSinkParamMap(project, absRoot);
|
|
992
1476
|
// G1(2026-09-19):路径校验识别。自身含校验证据的函数名 + 向调用方传播一跳。
|
|
993
|
-
|
|
1477
|
+
// direct = tier-0(自身含校验证据);all = 再向调用方传播一跳后的集合。
|
|
1478
|
+
// G2 只用 direct —— 传播得到的名字是「被推断为守卫」,拿它做抑制会把
|
|
1479
|
+
// 推断误差直接放大成误报消除。
|
|
1480
|
+
const { all: guardFns, direct: directGuardFns } = pathGuardFunctionNames(project);
|
|
1481
|
+
// C4b:项目自有「纯塑形」helper —— 让污点能穿过 getFileNamePath(...) 这类
|
|
1482
|
+
// 自建封装。与 guardFns 方向相反:那边是压掉流,这边是接通流,
|
|
1483
|
+
// 所以它的判据更严(三条全中才认),认不出就留着漏报。
|
|
1484
|
+
const shapers = pureShaperFunctionNames(project);
|
|
994
1485
|
const pendingMethodMarks = new Set();
|
|
995
1486
|
const onMethodHit = (rec) => {
|
|
996
1487
|
for (const me of rec.entries)
|
|
@@ -1008,7 +1499,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
1008
1499
|
const fParams = f.getParameters();
|
|
1009
1500
|
const fText = f.getText();
|
|
1010
1501
|
const fCalls = extractDirectCalls(f, fText);
|
|
1011
|
-
fCalls.push(...computeMarkerCalls(fText, fParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns));
|
|
1502
|
+
fCalls.push(...computeMarkerCalls(fText, fParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
|
|
1012
1503
|
funcs.push({
|
|
1013
1504
|
name,
|
|
1014
1505
|
params: fParams.map(p => ({
|
|
@@ -1038,7 +1529,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
1038
1529
|
const initParams = init.getParameters();
|
|
1039
1530
|
const initText = init.getText();
|
|
1040
1531
|
const initCalls = extractDirectCalls(init, initText);
|
|
1041
|
-
initCalls.push(...computeMarkerCalls(initText, initParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns));
|
|
1532
|
+
initCalls.push(...computeMarkerCalls(initText, initParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
|
|
1042
1533
|
funcs.push({
|
|
1043
1534
|
name,
|
|
1044
1535
|
params: initParams.map(p => ({
|
|
@@ -1064,7 +1555,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
1064
1555
|
const argParams = arg.getParameters();
|
|
1065
1556
|
const argText = arg.getText();
|
|
1066
1557
|
const argCalls = extractDirectCalls(arg, argText);
|
|
1067
|
-
argCalls.push(...computeMarkerCalls(argText, argParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns));
|
|
1558
|
+
argCalls.push(...computeMarkerCalls(argText, argParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers));
|
|
1068
1559
|
funcs.push({
|
|
1069
1560
|
name,
|
|
1070
1561
|
params: argParams.map(p => ({
|
|
@@ -1098,7 +1589,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
1098
1589
|
continue;
|
|
1099
1590
|
const mParams = m.getParameters();
|
|
1100
1591
|
const mText = m.getText();
|
|
1101
|
-
const mCalls = computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns);
|
|
1592
|
+
const mCalls = computeMarkerCalls(mText, mParams.map((p) => p.getName()), sinkParams, onMethodHit, guardFns, directGuardFns, shapers);
|
|
1102
1593
|
funcs.push({
|
|
1103
1594
|
name: `${cn}.${mn}`,
|
|
1104
1595
|
params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
|