progmune-runtime 3.7.30 → 3.7.31
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 +24 -0
- package/README.md +6 -3
- package/dist/extract-ir-ssrf-loop.test.js +123 -0
- package/dist/extract-ir.js +15 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.31] — 2026-09-18
|
|
4
|
+
|
|
5
|
+
### 提取器 SSRF 标记正则死循环修复(P0,真实工程可用性故障)
|
|
6
|
+
|
|
7
|
+
- **现象**:IR 提取阶段对含 `fetch(` 的 TS 文件 100% CPU 挂死——一个 55 行的切片 >480s 未完成,而 108 KB / 5 文件的语料只需 35s。实测 **nuxt-modules/og-image、tinacms/tinacms、zereight/gitlab-mcp 三个独立仓库全部触发**
|
|
8
|
+
- **定位**:对挂住进程做 CPU 采样(`sample`),**2193/2193 采样全部落在 `Builtins_RegExpPrototypeExec`**——确定为正则死循环,而非此前怀疑的 OOM / ts-morph 依赖解析
|
|
9
|
+
- **根因**(`src/extract-ir.ts` SSRF 分支,3.7.28 引入):`TS_HTTP_FETCH_SINK` 是**无 `g` 标志**的正则,却用于 `while ((m = TS_HTTP_FETCH_SINK.exec(text)) !== null)` 迭代。非全局正则的 `exec()` 忽略 `lastIndex` 并恒返回首个匹配;当首个 `fetch(` 后的 300 字符窗口不含污点时(真实代码里最常见的「良性 fetch」形态),循环无出口。同文件另 7 处 `while-exec` 循环均带 `g`,仅此一处遗漏
|
|
10
|
+
- **修复**:新增 `tsHttpFetchSinkIter()`(同模式 `"g"` 版)专供迭代;布尔探测仍用无标志版本(带 `g` 的 `.test()` 会污染 `lastIndex`)。模式至少匹配 5 字符,不可能零宽匹配
|
|
11
|
+
- **影响面**:任何含 `fetch(` 且函数体内无 SSRF 守卫词汇(`localhost|ssrf|hostname|…`)的 TS 工程都会挂死提取阶段——这是面向真实代码的普遍故障,不是基准内部的边缘情况
|
|
12
|
+
- **验收**:og-image 480s+ → **48s**;tinacms 300s+ → **57s**;gitlab-mcp 切片 480s+ → **80s**;最小复现 70s+ → **18s**
|
|
13
|
+
|
|
14
|
+
### 修复回归语料扩样至 17 条(fr-012 ~ fr-017)
|
|
15
|
+
|
|
16
|
+
- **样本结构**:open-webui 占比 75% → **41%**,独立仓库 3 → **10**(gitlab-mcp ×2、mockoon、og-image、redocly-cli、tinacms)
|
|
17
|
+
- **结果**:6 条新语料全部 MISS(pre/post 均 0 违规),全库 **DETECTED 4/17、MISS 13/17、PENDING 0**
|
|
18
|
+
- **正向对照**:为排除「0 违规是哑值」,scan `demo-project/` 报出 1 条真实违规(`SSG_AUTH_STATE_VIOLATION`,`token_lifecycle_flow.ts::main`)——引擎是活的
|
|
19
|
+
- **注册数据修正**:fr-013 的 parent SHA 末 4 位转置(codeload 404)且原 fix_commit 是纯 CHANGELOG 文档提交,经 PR #624 正文 link 定位到真修复 PR #571;fr-012 锚点错位(其「文件读取」半边由更早的 PR #482 修复)
|
|
20
|
+
- **新根因类别**:fr-012(MCP 工具实参 `args.file_path`)、fr-015(`decodeURIComponent` 解码后的对象字段 `font.path`)暴露**污点源词汇表缺口**——与 skyvern/open-webui 已暴露的认证词表缺口同族,共同病灶是「标记管线靠名字识源,不靠数据流语义」
|
|
21
|
+
- 详见 `blind-benchmark/REALWORLD_FIX_REGRESSION_V2.md`
|
|
22
|
+
|
|
23
|
+
### 回归
|
|
24
|
+
|
|
25
|
+
- 新增 `src/extract-ir-ssrf-loop.test.ts` 2 green ✓(①良性 fetch 必须正常返回——修复前永不返回;②URL 形参流入 fetch 仍必须注入 `__progmune_ssrf_user_url__`)/ build ✓
|
|
26
|
+
|
|
3
27
|
## [3.7.30] — 2026-09-17
|
|
4
28
|
|
|
5
29
|
### 提取器性能重构(3.7.28 引入的回归修复)
|
package/README.md
CHANGED
|
@@ -109,15 +109,18 @@ Progmune is honest about what it can and cannot verify.
|
|
|
109
109
|
| **Python** | ✅ Production | Blind benchmark: **recall 100% / precision 100%** (729 gold findings, 90 projects); real-world validation: PyGoat (OWASP vulnerable-by-design Django app) **67 TP / 0 FP, 100% labeled precision**; three well-written apps (django/fastapi realworld, django-unicorn) with 0 false-positive true findings |
|
|
110
110
|
| **C** | ✅ Annotation-driven (Beta) | IR extraction via the registry + SSG state machine; **annotate ~2-3 protocol primitives per protocol to get trusted verification** (gold 5/5 on real modules: redis ACL / libssh client / libssh server / libssh callback-dispatch / uftpd transfer-auth — all 0 FP with precise violation localization; app-level gold v2: **P=91.7% / R=100% / F1=95.7%**). Unannotated auto-detection stays out of scope (0 TP on real corpus — positioning decision recorded in the C Language Status doc); TLS-level coverage still absent. See [C Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/c-language-status.md). |
|
|
111
111
|
| **Go** | ✅ Annotation-driven (Beta) | IR extraction via registry + SSG state machine; synthetic gold v1: **P=100% / R=100%** (clean×3 + violations×3); zero external toolchain (pure-TS lexical extractor — works from npm installs) |
|
|
112
|
-
| **Java** |
|
|
112
|
+
| **Java** | 🔶 Annotation-driven (Beta) | Two layers, verified on real corpora. **Framework layer ✅**: Spring adapter (`spring-detector.ts` — security rule ordering + ant patterns + controller annotation routes) on gothinkster/spring-boot-realworld (1581★): 19 routes / 12 mutations fully parsed, 0 issues, catch-all flip counter-proof ×10; Spring Boot 3 modern dialect re-verified (SecurityFilterChain bean + String[] allowlist expansion): 15 routes, 0 issues. **Core layer**: pure-TS lexical extractor (zero toolchain deps) at **100%/100% recovery vs a tree-sitter AST baseline (1216/1216)**; SSG annotation-driven protocol rows (token / auth-register / resource) engine-locked with a real-corpus closed loop (**1 real TP / 0 FP**). Unannotated auto-detection stays out of scope (same decision as C/Go). Production label pending enterprise PoC. See [Java Language Status](https://github.com/shenlian19831109/progmune-runtime/blob/main/docs/java-language-status.md). |
|
|
113
113
|
|
|
114
114
|
**Multi-language IR (registry-based).** TypeScript (ts-morph) and Python (AST) extractors are registered entries in one registry (`src/extract-project-ir.ts`); `extractProjectIR` merges every detected language into a single function IR shared by the agent loop, `execute()`, and the MCP server — the agent composes function-protocol chains across both languages. Adding a language (Go, Java, …) = register one extractor entry; callers don't change.
|
|
115
115
|
|
|
116
|
-
**Framework adapters:
|
|
116
|
+
**Framework adapters: 13 dedicated detectors — 4 structural (AST-based) + 8 heuristic (code-string) + Spring Boot (security rule-ordering model); 13/13 now validated on real corpora.** Express ✅, tRPC ✅, FastAPI ✅, Django ✅ (structural route-level auth analysis — every mutation route checked for auth), Flask ✅ (route/before_request auth guard analysis), Fastify ✅ (route preHandler/hook auth analysis) and Next.js ✅ (App Router route-handler auth analysis), NestJS ✅ (decorator route parsing + global APP_GUARD recognition + @Public exemption), Koa ✅, Hapi ✅ (route-config auth strategy analysis), Gin ✅ and Fiber ✅ (Go route/auth-middleware analysis) and Spring Boot ✅ (Spring Security rule ordering; Boot 2 + Boot 3 dialects) have dedicated detectors. Next.js version-aware governance also exists.
|
|
117
|
+
|
|
118
|
+
**Evidence tiers** (adopted 2026-09-02): 🟡 synthetic → ✅ **real-corpus validated** (≥1 real production corpus at 0 protocol-level FP + guard-removal counter-proof) → 🏭 production (enterprise PoC/adoption — none yet). **All 13/13 are ✅.** Documented boundaries: Express `main.ts` retains a NO_HELMET hardening gap (not protocol-level); the NestJS corpus yields 1 real TP (`DELETE /users/:slug`); jiotv_go's 2 flags are capability-token shapes; gofiber recipes flags are demo-context noise (no auth intent). Remaining gap: a few TS frameworks and Go frameworks beyond Gin/Fiber.
|
|
117
119
|
|
|
118
120
|
### What Progmune does NOT cover (honest boundaries)
|
|
119
121
|
|
|
120
|
-
- **TS-side taint-based injection flaws
|
|
122
|
+
- **TS-side taint-based injection flaws — partial.** Python ships source-level SQLi/XSS/SSRF. On the TypeScript side, **path traversal (3.7.26)** and **SSRF (3.7.28)** are covered: the extractor emits markers (request/param taint → fs or HTTP sink, one hop + one cross-function hop) and the engine consumes them at the IR layer — validated against real security fixes at their ground-truth sites (fr-007 openhop, fr-011 mcp-from-openapi). **SQLi and XSS remain uncovered**: the TS extractor is name/call-based, not a dataflow taint engine, so those classes stay out of scope (documented, not hidden).
|
|
123
|
+
- **Missing-check (absence) vulnerabilities** — the dominant real-world fix shape. A protocol state machine proves "these calls happened in the wrong order"; it does not prove "a required check was never called." Authorisation-policy bypasses, allowlist-path omissions, and ownership checks missing on one route are the largest uncovered class — measured, not guessed: see the [real-fix regression corpus](https://github.com/shenlian19831109/progmune-runtime/blob/main/blind-benchmark/fix-regression-corpus.json).
|
|
121
124
|
- **SCA / dependency vulnerabilities** — hallucinated package names, supply-chain issues. Separate tooling exists for this.
|
|
122
125
|
- **Runtime behavior** — Progmune is static analysis only; no DAST/sandbox execution.
|
|
123
126
|
- **Framework internals** — well-known framework dispatch/cache machinery (e.g. django-unicorn internals) can produce a small number of boundary false positives; they are documented per-corpus in the benchmark gold files.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SSRF 标记提取的死循环回归(2026-09-18,P0)。
|
|
4
|
+
*
|
|
5
|
+
* 根因:computeMarkerCalls 的 SSRF 分支对一个**无 `g` 标志**的正则
|
|
6
|
+
* TS_HTTP_FETCH_SINK 做 while-exec 迭代。非全局正则的 exec() 忽略
|
|
7
|
+
* lastIndex 并恒返回首个匹配,因此只要首个 fetch 调用后的 300 字符窗口
|
|
8
|
+
* 内不含污点——真实代码里最常见的「良性 fetch」形态——循环就没有出口,
|
|
9
|
+
* 提取阶段 100% CPU 死循环。CPU 采样佐证:2193/2193 采样全部落在
|
|
10
|
+
* Builtins_RegExpPrototypeExec。
|
|
11
|
+
*
|
|
12
|
+
* 影响面:任何含 `fetch(` 且函数体内无 SSRF 守卫词汇的 TS 工程都会挂死
|
|
13
|
+
* 提取阶段(实测 nuxt og-image、tinacms、gitlab-mcp 三个独立仓库均触发)。
|
|
14
|
+
*
|
|
15
|
+
* 本文件锁定两端:
|
|
16
|
+
* 1. 良性 fetch 必须正常返回(修复前永不返回);
|
|
17
|
+
* 2. 真正的 SSRF 污点仍必须注入 __progmune_ssrf_user_url__(修复不得削弱检出)。
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
const vitest_1 = require("vitest");
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const os = __importStar(require("os"));
|
|
56
|
+
const path = __importStar(require("path"));
|
|
57
|
+
const extract_ir_1 = require("./extract-ir");
|
|
58
|
+
const TSCONFIG = JSON.stringify({
|
|
59
|
+
compilerOptions: {
|
|
60
|
+
target: "ES2020",
|
|
61
|
+
module: "commonjs",
|
|
62
|
+
moduleResolution: "node",
|
|
63
|
+
strict: false,
|
|
64
|
+
skipLibCheck: true,
|
|
65
|
+
noEmit: true,
|
|
66
|
+
},
|
|
67
|
+
include: ["**/*.ts"],
|
|
68
|
+
});
|
|
69
|
+
function makeProject(files) {
|
|
70
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-ssrf-loop-"));
|
|
71
|
+
fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
|
|
72
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
|
|
73
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
74
|
+
const abs = path.join(dir, rel);
|
|
75
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
76
|
+
fs.writeFileSync(abs, body);
|
|
77
|
+
}
|
|
78
|
+
return dir;
|
|
79
|
+
}
|
|
80
|
+
(0, vitest_1.describe)("SSRF 标记提取:良性 fetch 不得使提取器死循环", () => {
|
|
81
|
+
(0, vitest_1.it)("无守卫词汇、无污点的 fetch 调用应正常完成提取", () => {
|
|
82
|
+
const dir = makeProject({
|
|
83
|
+
"api.ts": `
|
|
84
|
+
export async function ping(): Promise<any> {
|
|
85
|
+
const response = await fetch("https://x.example/uploads", { method: "POST" });
|
|
86
|
+
return response.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function pong(): Promise<any> {
|
|
90
|
+
return fetch("https://x.example/other");
|
|
91
|
+
}
|
|
92
|
+
`,
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
const fns = (0, extract_ir_1.extractIR)(dir);
|
|
96
|
+
const names = fns.map((f) => f.name);
|
|
97
|
+
(0, vitest_1.expect)(names).toContain("ping");
|
|
98
|
+
(0, vitest_1.expect)(names).toContain("pong");
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}, 150000);
|
|
104
|
+
(0, vitest_1.it)("URL 形参直接流入 fetch 仍应注入 __progmune_ssrf_user_url__", () => {
|
|
105
|
+
const dir = makeProject({
|
|
106
|
+
"proxy.ts": `
|
|
107
|
+
export async function proxy(target: string): Promise<any> {
|
|
108
|
+
const response = await fetch(target);
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
});
|
|
113
|
+
try {
|
|
114
|
+
const fns = (0, extract_ir_1.extractIR)(dir);
|
|
115
|
+
const proxy = fns.find((f) => f.name === "proxy");
|
|
116
|
+
(0, vitest_1.expect)(proxy).toBeDefined();
|
|
117
|
+
(0, vitest_1.expect)(proxy.calls ?? []).toContain("__progmune_ssrf_user_url__");
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}, 150000);
|
|
123
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -516,6 +516,19 @@ function methodSinkParamMap(project, absRoot) {
|
|
|
516
516
|
// Web 形态(req.params)共用同一标记;引擎 IR 层消费。
|
|
517
517
|
// ═══════════════════════════════════════════════════════════════
|
|
518
518
|
const TS_HTTP_FETCH_SINK = /\b(fetch|axios\.(?:get|post|put|delete|head|patch)|http\.request|https\.request|ky\.(?:get|post|put|delete|head|patch)|undici\.request|nodeFetch|got)\s*\(/;
|
|
519
|
+
/**
|
|
520
|
+
* 同模式的**全局**版本,仅用于 while-exec 迭代。
|
|
521
|
+
*
|
|
522
|
+
* 2026-09-18 P0 修复:此前 while 循环直接对无 `g` 标志的 TS_HTTP_FETCH_SINK
|
|
523
|
+
* 反复调用 exec()。非全局正则的 exec() 会忽略 lastIndex 并恒返回首个匹配,
|
|
524
|
+
* 一旦首个匹配的 300 字符窗口内不含污点(真实代码里最常见的良性 fetch
|
|
525
|
+
* 形态),循环无出口 → 提取器 100% CPU 死循环。CPU 采样证实:
|
|
526
|
+
* 2193/2193 采样全部落在 Builtins_RegExpPrototypeExec。
|
|
527
|
+
* 该缺陷会使任何含 `fetch(` 且无 SSRF 守卫词汇的 TS 工程挂死提取阶段。
|
|
528
|
+
*/
|
|
529
|
+
function tsHttpFetchSinkIter() {
|
|
530
|
+
return new RegExp(TS_HTTP_FETCH_SINK.source, "g");
|
|
531
|
+
}
|
|
519
532
|
const SSRF_GUARD_EVIDENCE = /127\.0\.0\.1|0\.0\.0\.0|169\.254\.|::1|localhost|isPrivate|isLoopback|hostname|denylist|blocklist|ssrf|validateUrl|isSafeUrl|getAddresses|ipaddress|forbidden_host|private_ip/i;
|
|
520
533
|
const URL_PARAM_NAME = /^(url|target|endpoint|link|href|webUrl|web_url|sourceUrl|source_url|remoteUrl|remote_url|fetchUrl|fetch_url|specUrl|spec_url|apiUrl|api_url|origin|baseUrl|base_url)$/i;
|
|
521
534
|
function collectUrlParamNames(params) {
|
|
@@ -569,9 +582,9 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit) {
|
|
|
569
582
|
taintParts.push(`\\b(?:${[...reqTainted].join("|")})\\b`);
|
|
570
583
|
taintParts.push(/\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.source);
|
|
571
584
|
const taint = new RegExp(taintParts.join("|"));
|
|
572
|
-
|
|
585
|
+
const fetchSinkIter = tsHttpFetchSinkIter();
|
|
573
586
|
let m;
|
|
574
|
-
while ((m =
|
|
587
|
+
while ((m = fetchSinkIter.exec(text)) !== null) {
|
|
575
588
|
const after = text.slice(m.index + m[0].length, m.index + m[0].length + 300);
|
|
576
589
|
if (taint.test(after)) {
|
|
577
590
|
markers.push("__progmune_ssrf_user_url__");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.31",
|
|
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/",
|