progmune-runtime 3.7.60 → 3.7.62
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 +76 -1
- package/dist/extract-ir-migrations.test.js +104 -0
- package/dist/extract-ir-request-anchored-identity.test.js +223 -0
- package/dist/extract-ir.js +49 -0
- package/dist/protocol-detector.js +17 -0
- package/dist/sdk.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [3.7.
|
|
3
|
+
## [3.7.62] — 2026-10-09
|
|
4
|
+
|
|
5
|
+
### 迁移脚本排除出应用面(修冤枉第三刀:§49.19 补标注后首个数据驱动修复)
|
|
6
|
+
|
|
7
|
+
**背景**:§49.19 补标注队列全部完成——docmost 174/174 条逐条人工核实
|
|
8
|
+
(gold_confidence=verified,verified_by=ai-read-source),**全部为 FP**。
|
|
9
|
+
这补齐了修冤枉第二刀撞到的数据边界(fp-pool 重扫 381→189 后「可干净
|
|
10
|
+
动刀的根因挖完」的剩余瓶颈就是标注不足)。
|
|
11
|
+
|
|
12
|
+
- **证据**:`blind-benchmark/reports/advisories/annotation-queue.jsonl`
|
|
13
|
+
(174 条全量 gold 字段落档)+ `blind-benchmark/fp-gold.jsonl`(+174 条
|
|
14
|
+
schema v2)+ `annotate-batches/`(16 批原始标注记录)
|
|
15
|
+
- **首个根因**:Kysely 迁移脚本 `up()/down()`(src/database/migrations/)
|
|
16
|
+
被多族规则误报——addColumn/createTable 词撞 Authorization
|
|
17
|
+
(Unauthenticated Mutation),dropTable 撞 Data Mutation 族。
|
|
18
|
+
迁移只在部署时跑一次、不暴露请求面,与 node_modules/benchmarks 同属
|
|
19
|
+
「无应用面」判定。
|
|
20
|
+
- **修复**:`src/extract-ir.ts` 提取循环跳过 `/migrations/` 路径
|
|
21
|
+
(与 node_modules 跳过并列;Python 侧 alembic 未在本轮测量,暂不动——
|
|
22
|
+
按三要件,没有数据不扩面)
|
|
23
|
+
- **A/B 实测**(docmost apps/server 同引擎双扫描):告警流 992 → 942
|
|
24
|
+
(−50,恰好等于 fullscan 里 47 条 Unauthenticated Mutation + 3 条
|
|
25
|
+
Ownership Check 的迁移脚本 FP);**判定/分数/主路径违规零变化**
|
|
26
|
+
(NEEDS_REVIEW 86 / 7 violations / auth 44,两侧完全一致)
|
|
27
|
+
- **回归锁定**:新测试 `src/extract-ir-migrations.test.ts`(2 例:迁移
|
|
28
|
+
不提取 + migrations 字样非目录不误伤);TS 盲测 119 项目零漂移
|
|
29
|
+
(仅时间戳变化);taintpath 160/160;fr-007 pre5/post0、fr-016
|
|
30
|
+
pre7/post0 维持
|
|
31
|
+
- 已知代价(如实记录):迁移文件里的真实问题(如数据回填 SQLi)不再
|
|
32
|
+
检出——与 node_modules 跳过同一取舍,提取面=应用面
|
|
33
|
+
|
|
34
|
+
## [3.7.61] — 2026-10-06
|
|
35
|
+
|
|
36
|
+
### fr-017 点亮:授权身份锚点取自请求输入(修复回归 11/17)
|
|
37
|
+
|
|
38
|
+
**归属语义漏报根因第一刀**:tinacms 的 `isAuthorized` 把「token 属于哪个
|
|
39
|
+
app/client」的身份锚点从 `req.query.clientID` 读取去问身份服务——攻击者
|
|
40
|
+
注册自己的 app 拿合法 token,即通过任意站点的后端授权(confused-deputy
|
|
41
|
+
变体,GHSA-g74q-6g2f-874x)。修复 = 锚点改从服务端配置取
|
|
42
|
+
(`expectedClientID ?? process.env.NEXT_PUBLIC_TINA_CLIENT_ID`)+ 取不到
|
|
43
|
+
fail-closed。
|
|
44
|
+
|
|
45
|
+
- 提取器 `src/extract-ir.ts`:`hasRequestAnchoredIdentity`——判据三元组
|
|
46
|
+
① 从请求取 token(req.headers.authorization / headers().get('authorization'))
|
|
47
|
+
② 从请求输入取 app/client 身份锚点(query/body/params 或 searchParams)
|
|
48
|
+
③ 无服务端锚点证据(process.env. / getConfig( / config.)
|
|
49
|
+
⇒ 注入 `__progmune_request_anchored_identity__`
|
|
50
|
+
- 规则 "Authorization Identity Anchor from Request"(ts/js,marker-driven,
|
|
51
|
+
进 safeguard 告警流,与 fr-001/002/003 同族)
|
|
52
|
+
- 边界(写死在判据里):锚点名不含 tenant(多租户从请求解析租户锚点是
|
|
53
|
+
合法形态);OAuth token 端点(req.body.client_id + client_secret,无
|
|
54
|
+
authorization header)不触发
|
|
55
|
+
- **三态验证**:修复前 2/2 真值函数命中(@tinacms/auth 与
|
|
56
|
+
next-tinacms-azure 两个变体)/ 修复后 0/0;TS 盲测 119 项目逐函数
|
|
57
|
+
零漂移(LOST 0/ADDED 0);fp-pool 真实语料 9 切片 1186 函数 0 命中;
|
|
58
|
+
定向测试 8/8。快照入库 `blind-benchmark/fr-corpus/fr-017-tinacms/`
|
|
59
|
+
|
|
60
|
+
### fr-008 / fr-013 判定关闭(修复回归语料全量定性收官)
|
|
61
|
+
|
|
62
|
+
- **fr-008**(SimpleWebAuthn,密码学协议语义):证书链 walk 在第一个自签名
|
|
63
|
+
证书处 break,永不检查链尾拼上的配置信任锚点——「链内部自洽」冒充
|
|
64
|
+
「链终止于信任锚点」(修复 = break 条件改为 `issuer.equal(anchor)`)。
|
|
65
|
+
检出需要建模证书链验证协议语义,任何标记/污点/缺席判据都够不着,
|
|
66
|
+
归入 V1 七类根因「密码学协议语义」,不做。
|
|
67
|
+
- **fr-013**(gitlab-mcp,五子问题全部分类):F1(a) GraphQL 逗号前导形态
|
|
68
|
+
= 字符串解析语义;F1(b) execute_graphql 缺项目范围检查 = 缺席型判据
|
|
69
|
+
**唯一可点亮方向**,但判据是 MCP 工具处理器专属词汇,需 MCP 类公告
|
|
70
|
+
语料族(≥2-3 条)支撑,单条按三要件不做;F2/F3 配置通道路由(启动校验
|
|
71
|
+
门缺凭证形态 / SSE 默认无认证 + DNS rebinding);F4 会话耗尽 DoS;
|
|
72
|
+
F5 内容级提示注入防御。constant-time 比较属加固建议类不单做。
|
|
73
|
+
- **fr-corpus 17 条全部定性收官:有效 11/17(9 干净 DETECTED + 2
|
|
74
|
+
with-boundary),6 条判定关闭**(fr-004 闭包晚绑定 / fr-006 黑名单
|
|
75
|
+
完备性 / fr-008 密码学协议语义 / fr-009 守卫不足 / fr-013 五子问题
|
|
76
|
+
全出检出面 / fr-015 corpus_invalid+守卫不足)。
|
|
77
|
+
|
|
78
|
+
## [3.7.60] — 2026-10-05
|
|
4
79
|
|
|
5
80
|
### fr-002/003 点亮:令牌交换路径检查完备性(修复回归 10/17)
|
|
6
81
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const vitest_1 = require("vitest");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const extract_ir_1 = require("./extract-ir");
|
|
41
|
+
/**
|
|
42
|
+
* /migrations/ 目录跳过(2026-10-08 §49.19 补标注的产物)。
|
|
43
|
+
*
|
|
44
|
+
* 数据:docmost 补标注队列 174 条里,迁移脚本 up()/down() 被多族规则报
|
|
45
|
+
* (Authorization 族 + Data Mutation 族——addColumn/createTable 词撞),
|
|
46
|
+
* 全部人工核实为 FP:迁移只在部署时跑一次,不暴露请求面,与
|
|
47
|
+
* node_modules/benchmarks 同属「无应用面」判定。
|
|
48
|
+
*
|
|
49
|
+
* 锁定行为:migrations 目录下的源文件整体不参与提取;
|
|
50
|
+
* 同项目其他目录正常提取(防误伤)。
|
|
51
|
+
*/
|
|
52
|
+
const TSCONFIG = JSON.stringify({
|
|
53
|
+
compilerOptions: {
|
|
54
|
+
target: "ES2020",
|
|
55
|
+
module: "commonjs",
|
|
56
|
+
moduleResolution: "node",
|
|
57
|
+
strict: false,
|
|
58
|
+
skipLibCheck: true,
|
|
59
|
+
noEmit: true,
|
|
60
|
+
},
|
|
61
|
+
include: ["**/*.ts"],
|
|
62
|
+
});
|
|
63
|
+
function makeProject(files) {
|
|
64
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-migrations-"));
|
|
65
|
+
fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
|
|
66
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
|
|
67
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
68
|
+
const abs = path.join(dir, rel);
|
|
69
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
70
|
+
fs.writeFileSync(abs, body);
|
|
71
|
+
}
|
|
72
|
+
return dir;
|
|
73
|
+
}
|
|
74
|
+
(0, vitest_1.describe)("extractIR:/migrations/ 目录跳过(无应用面判定)", () => {
|
|
75
|
+
(0, vitest_1.it)("迁移脚本不提取,同项目其他目录正常提取", () => {
|
|
76
|
+
const dir = makeProject({
|
|
77
|
+
"src/app.ts": `export function handleCreatePage(): void {
|
|
78
|
+
console.log("app surface");
|
|
79
|
+
}`,
|
|
80
|
+
"src/database/migrations/20260101T000000-init.ts": `import { Kysely } from "kysely";
|
|
81
|
+
export async function up(db: Kysely<any>): Promise<void> {
|
|
82
|
+
await db.schema.createTable("page").addColumn("id", "text").execute();
|
|
83
|
+
}
|
|
84
|
+
export async function down(db: Kysely<any>): Promise<void> {
|
|
85
|
+
await db.schema.dropTable("page").execute();
|
|
86
|
+
}`,
|
|
87
|
+
});
|
|
88
|
+
const fns = (0, extract_ir_1.extractIR)(dir).map((f) => f.name).sort();
|
|
89
|
+
// 应用面函数保留
|
|
90
|
+
(0, vitest_1.expect)(fns).toContain("handleCreatePage");
|
|
91
|
+
// 迁移脚本 up/down 不参与提取
|
|
92
|
+
(0, vitest_1.expect)(fns).not.toContain("up");
|
|
93
|
+
(0, vitest_1.expect)(fns).not.toContain("down");
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.it)("路径含 migrations 字样但不属于 /migrations/ 目录的文件不误伤", () => {
|
|
96
|
+
const dir = makeProject({
|
|
97
|
+
"src/migrations-service.ts": `export function runMigrations(): void {
|
|
98
|
+
console.log("service, not a migration dir");
|
|
99
|
+
}`,
|
|
100
|
+
});
|
|
101
|
+
const fns = (0, extract_ir_1.extractIR)(dir).map((f) => f.name);
|
|
102
|
+
(0, vitest_1.expect)(fns).toContain("runMigrations");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* fr-017 定向回归:授权身份锚点取自请求输入
|
|
4
|
+
* (REALWORLD_FIX_REGRESSION fr-017,tinacms GHSA-g74q-6g2f-874x)
|
|
5
|
+
*
|
|
6
|
+
* 判据在 extract-ir.ts 的 hasRequestAnchoredIdentity(三元组):
|
|
7
|
+
* ① 从请求取 token(req.headers.authorization / headers().get('authorization'))
|
|
8
|
+
* ② 从请求输入取身份锚点(req.query|body|params.<clientID|clientId|appId…>
|
|
9
|
+
* 或 searchParams.get('<锚点名>'))
|
|
10
|
+
* ③ 无服务端锚点证据(process.env. / getConfig( / config.)
|
|
11
|
+
* 三条件同时成立才注入 __progmune_request_anchored_identity__ 标记。
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
const vitest_1 = require("vitest");
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const extract_ir_1 = require("./extract-ir");
|
|
52
|
+
const protocol_detector_1 = require("./protocol-detector");
|
|
53
|
+
const TSCONFIG = JSON.stringify({
|
|
54
|
+
compilerOptions: {
|
|
55
|
+
target: "ES2020",
|
|
56
|
+
module: "commonjs",
|
|
57
|
+
moduleResolution: "node",
|
|
58
|
+
strict: false,
|
|
59
|
+
skipLibCheck: true,
|
|
60
|
+
noEmit: true,
|
|
61
|
+
},
|
|
62
|
+
include: ["**/*.ts"],
|
|
63
|
+
});
|
|
64
|
+
function makeProject(files) {
|
|
65
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-fr017-"));
|
|
66
|
+
fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
|
|
67
|
+
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
|
|
68
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
69
|
+
const abs = path.join(dir, rel);
|
|
70
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
71
|
+
fs.writeFileSync(abs, body);
|
|
72
|
+
}
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
const MARKER = "__progmune_request_anchored_identity__";
|
|
76
|
+
(0, vitest_1.describe)("fr-017 request-anchored identity(提取器标记)", () => {
|
|
77
|
+
(0, vitest_1.it)("修复前形态:req.query.clientID 锚点 + authorization header → 注入标记", () => {
|
|
78
|
+
const dir = makeProject({
|
|
79
|
+
"index.ts": `
|
|
80
|
+
export const isAuthorized = async (req: any): Promise<any> => {
|
|
81
|
+
const clientID = req.query.clientID;
|
|
82
|
+
const token = req.headers.authorization;
|
|
83
|
+
if (typeof clientID === 'string' && typeof token === 'string') {
|
|
84
|
+
return await isUserAuthorized({ clientID, token });
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
async function isUserAuthorized(args: { clientID: string; token: string }) {
|
|
90
|
+
return fetch("https://identity.example/verify", {
|
|
91
|
+
headers: { authorization: args.token },
|
|
92
|
+
body: JSON.stringify({ clientID: args.clientID }),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
`,
|
|
96
|
+
});
|
|
97
|
+
const ir = (0, extract_ir_1.extractIR)(dir);
|
|
98
|
+
const fn = ir.find((f) => f.name === "isAuthorized");
|
|
99
|
+
const callee = ir.find((f) => f.name === "isUserAuthorized");
|
|
100
|
+
(0, vitest_1.expect)(fn).toBeDefined();
|
|
101
|
+
(0, vitest_1.expect)(fn.calls).toContain(MARKER);
|
|
102
|
+
// 被调用方没有 req 形态,不误标
|
|
103
|
+
(0, vitest_1.expect)(callee).toBeDefined();
|
|
104
|
+
(0, vitest_1.expect)(callee.calls).not.toContain(MARKER);
|
|
105
|
+
});
|
|
106
|
+
(0, vitest_1.it)("修复前形态(NextRequest):searchParams.get('clientID') → 注入标记", () => {
|
|
107
|
+
const dir = makeProject({
|
|
108
|
+
"auth.ts": `
|
|
109
|
+
export const isAuthorized = async (req: Request): Promise<any> => {
|
|
110
|
+
const clientID = new URL(req.url).searchParams.get('clientID');
|
|
111
|
+
const token = (await headers()).get('authorization');
|
|
112
|
+
if (typeof clientID === 'string' && typeof token === 'string') {
|
|
113
|
+
return await isUserAuthorized({ clientID, token });
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
async function isUserAuthorized(args: { clientID: string; token: string }) {
|
|
119
|
+
return fetch("https://identity.example/verify");
|
|
120
|
+
}
|
|
121
|
+
`,
|
|
122
|
+
});
|
|
123
|
+
const ir = (0, extract_ir_1.extractIR)(dir);
|
|
124
|
+
const fn = ir.find((f) => f.name === "isAuthorized");
|
|
125
|
+
(0, vitest_1.expect)(fn).toBeDefined();
|
|
126
|
+
(0, vitest_1.expect)(fn.calls).toContain(MARKER);
|
|
127
|
+
});
|
|
128
|
+
(0, vitest_1.it)("修复后形态:expectedClientID ?? process.env → 无标记", () => {
|
|
129
|
+
const dir = makeProject({
|
|
130
|
+
"index.ts": `
|
|
131
|
+
export const isAuthorized = async (req: any, expectedClientID?: string): Promise<any> => {
|
|
132
|
+
const token = req.headers.authorization;
|
|
133
|
+
const clientID = (expectedClientID ?? process.env.NEXT_PUBLIC_TINA_CLIENT_ID)?.trim();
|
|
134
|
+
if (typeof clientID !== 'string' || clientID.length === 0) {
|
|
135
|
+
console.error("isAuthorized could not resolve this site's clientID. Refusing to authorize.");
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (typeof token !== 'string') return undefined;
|
|
139
|
+
return await isUserAuthorized({ clientID, token });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
async function isUserAuthorized(args: { clientID: string; token: string }) {
|
|
143
|
+
return fetch("https://identity.example/verify");
|
|
144
|
+
}
|
|
145
|
+
`,
|
|
146
|
+
});
|
|
147
|
+
const ir = (0, extract_ir_1.extractIR)(dir);
|
|
148
|
+
for (const f of ir) {
|
|
149
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
(0, vitest_1.it)("负例:OAuth token 端点(req.body.client_id,无 authorization header)→ 无标记", () => {
|
|
153
|
+
const dir = makeProject({
|
|
154
|
+
"token.ts": `
|
|
155
|
+
export const tokenEndpoint = async (req: any) => {
|
|
156
|
+
const clientId = req.body.client_id;
|
|
157
|
+
const clientSecret = req.body.client_secret;
|
|
158
|
+
return await exchangeToken({ clientId, clientSecret });
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
async function exchangeToken(args: { clientId: string; clientSecret: string }) {
|
|
162
|
+
return fetch("https://identity.example/token");
|
|
163
|
+
}
|
|
164
|
+
`,
|
|
165
|
+
});
|
|
166
|
+
const ir = (0, extract_ir_1.extractIR)(dir);
|
|
167
|
+
for (const f of ir) {
|
|
168
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
(0, vitest_1.it)("负例:锚点有服务端绑定(getConfig)→ 无标记", () => {
|
|
172
|
+
const dir = makeProject({
|
|
173
|
+
"index.ts": `
|
|
174
|
+
export const isAuthorized = async (req: any): Promise<any> => {
|
|
175
|
+
const token = req.headers.authorization;
|
|
176
|
+
const clientID = req.query.clientID;
|
|
177
|
+
const expected = getConfig("client-id", "CLIENT_ID");
|
|
178
|
+
if (clientID !== expected) return undefined;
|
|
179
|
+
return await isUserAuthorized({ clientID: expected, token });
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
async function isUserAuthorized(args: { clientID: string; token: string }) {
|
|
183
|
+
return fetch("https://identity.example/verify");
|
|
184
|
+
}
|
|
185
|
+
`,
|
|
186
|
+
});
|
|
187
|
+
const ir = (0, extract_ir_1.extractIR)(dir);
|
|
188
|
+
for (const f of ir) {
|
|
189
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
(0, vitest_1.it)("负例:tenantId 从请求解析(多租户合法形态)→ 无标记", () => {
|
|
193
|
+
const dir = makeProject({
|
|
194
|
+
"index.ts": `
|
|
195
|
+
export const isAuthorized = async (req: any): Promise<any> => {
|
|
196
|
+
const tenantId = req.query.tenantId;
|
|
197
|
+
const token = req.headers.authorization;
|
|
198
|
+
return await verifyTenantToken({ tenantId, token });
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
async function verifyTenantToken(args: { tenantId: string; token: string }) {
|
|
202
|
+
return fetch("https://identity.example/verify");
|
|
203
|
+
}
|
|
204
|
+
`,
|
|
205
|
+
});
|
|
206
|
+
const ir = (0, extract_ir_1.extractIR)(dir);
|
|
207
|
+
for (const f of ir) {
|
|
208
|
+
(0, vitest_1.expect)(f.calls).not.toContain(MARKER);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
(0, vitest_1.describe)("fr-017 request-anchored identity(规则触发)", () => {
|
|
213
|
+
(0, vitest_1.it)("TS 语言:标记命中 Authorization Identity Anchor from Request", () => {
|
|
214
|
+
const svs = (0, protocol_detector_1.detectSafeguardViolations)([MARKER], "isAuthorized", "typescript", []);
|
|
215
|
+
const hit = svs.find((v) => v.rule === "Authorization Identity Anchor from Request");
|
|
216
|
+
(0, vitest_1.expect)(hit).toBeDefined();
|
|
217
|
+
});
|
|
218
|
+
(0, vitest_1.it)("python 语言:该规则不触发(languages 门)", () => {
|
|
219
|
+
const svs = (0, protocol_detector_1.detectSafeguardViolations)([MARKER], "is_authorized", "python", []);
|
|
220
|
+
const hit = svs.find((v) => v.rule === "Authorization Identity Anchor from Request");
|
|
221
|
+
(0, vitest_1.expect)(hit).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
});
|
package/dist/extract-ir.js
CHANGED
|
@@ -2891,6 +2891,41 @@ const URL_PARAM_NAME = /^(url|target|endpoint|link|href|webUrl|web_url|sourceUrl
|
|
|
2891
2891
|
function collectUrlParamNames(params) {
|
|
2892
2892
|
return params.map((p) => p.name).filter((n) => URL_PARAM_NAME.test(n));
|
|
2893
2893
|
}
|
|
2894
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2895
|
+
// 请求锚定身份标记(2026-10-06,fr-017 tinacms GHSA-g74q-6g2f-874x)
|
|
2896
|
+
//
|
|
2897
|
+
// 身份校验把「token 属于哪个 app/client」这一身份锚点从**请求输入**读取
|
|
2898
|
+
// (req.query.clientID / searchParams.get('clientID'))。攻击者注册自己的
|
|
2899
|
+
// app 拿到合法 token,再对受害者后端指定自己的 clientID——后端问身份服务
|
|
2900
|
+
// 「这 token 是不是该 app 的」→ 是 → 授权通过。修复形态 = 锚点改从服务端
|
|
2901
|
+
// 配置取(expectedClientID ?? process.env.NEXT_PUBLIC_TINA_CLIENT_ID),
|
|
2902
|
+
// 且取不到 fail-closed。
|
|
2903
|
+
//
|
|
2904
|
+
// 判据三元组(与 fr-002/003 的缺席判据同族):
|
|
2905
|
+
// ① 从请求取 token(req.headers.authorization / headers().get('authorization'))
|
|
2906
|
+
// ② 从请求输入取身份锚点(req.query|body|params.<锚点名> 或 searchParams.get)
|
|
2907
|
+
// ③ 无服务端锚点证据(process.env. / getConfig( / config.)
|
|
2908
|
+
// 锚点名不含 tenant——多租户从请求解析租户锚点是合法形态,只有 app/client
|
|
2909
|
+
// 身份才是「服务器该知道自己的身份」的一侧。
|
|
2910
|
+
// 消费方:protocol-detector 的 "Authorization Identity Anchor from Request"。
|
|
2911
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2912
|
+
const REQUEST_ANCHORED_IDENTITY_MARKER = "__progmune_request_anchored_identity__";
|
|
2913
|
+
const IDENTITY_ANCHOR_NAMES = /clientID|clientId|client_id|appId|appID|app_id|applicationId|applicationID|application_id/;
|
|
2914
|
+
/** ① 从请求取 token(NextApiRequest 与 NextRequest 两种形态) */
|
|
2915
|
+
const REQ_TOKEN_SRC = /req\.headers\.authorization|\.get\(\s*["']authorization["']\s*\)/i;
|
|
2916
|
+
/** ② 从请求输入取身份锚点(query/body/params 成员或 searchParams.get) */
|
|
2917
|
+
const REQ_ANCHOR_SRC = new RegExp(`(?:req\\.(?:query|body|params)\\.(?:${IDENTITY_ANCHOR_NAMES.source})\\b|searchParams\\.get\\(\\s*["'](?:${IDENTITY_ANCHOR_NAMES.source})["']\\s*\\))`);
|
|
2918
|
+
/** ③ 服务端锚点证据:出现即视为锚点有服务端绑定(配置/环境/配置对象),不标记 */
|
|
2919
|
+
const SERVER_ANCHOR_EVIDENCE = /process\.env\.|\bgetConfig\s*\(|\bconfig\./i;
|
|
2920
|
+
function hasRequestAnchoredIdentity(text) {
|
|
2921
|
+
if (!REQ_TOKEN_SRC.test(text))
|
|
2922
|
+
return false;
|
|
2923
|
+
if (!REQ_ANCHOR_SRC.test(text))
|
|
2924
|
+
return false;
|
|
2925
|
+
if (SERVER_ANCHOR_EVIDENCE.test(text))
|
|
2926
|
+
return false;
|
|
2927
|
+
return true;
|
|
2928
|
+
}
|
|
2894
2929
|
/**
|
|
2895
2930
|
* 标记增强的内联实现(2026-09-15 性能重构):消费主循环已获取的
|
|
2896
2931
|
* 函数文本与形参名,输出该函数应附加的合成标记。
|
|
@@ -2974,6 +3009,12 @@ function computeMarkerCalls(text, paramNames, sinkParams, onMethodHit, guardFns,
|
|
|
2974
3009
|
}
|
|
2975
3010
|
}
|
|
2976
3011
|
}
|
|
3012
|
+
// ── 请求锚定身份(2026-10-06,fr-017)──
|
|
3013
|
+
// 纯函数体文本判据,无需函数名——isUserAuthorized(被调用方)体内没有
|
|
3014
|
+
// req.headers.authorization 形态,不会误标;只有面向请求的校验入口会命中。
|
|
3015
|
+
if (hasRequestAnchoredIdentity(text)) {
|
|
3016
|
+
markers.push(REQUEST_ANCHORED_IDENTITY_MARKER);
|
|
3017
|
+
}
|
|
2977
3018
|
return markers;
|
|
2978
3019
|
}
|
|
2979
3020
|
/**
|
|
@@ -3095,6 +3136,14 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
3095
3136
|
for (const sf of project.getSourceFiles()) {
|
|
3096
3137
|
if (sf.getFilePath().includes("node_modules"))
|
|
3097
3138
|
continue; // lib.d.ts 等声明文件只贡献噪声(2026-09-17 性能)
|
|
3139
|
+
// 2026-10-08(§49.19 补标注 174/174 的证据):迁移脚本不是应用表面——
|
|
3140
|
+
// Kysely 的 addColumn/createTable/insertInto 词撞变更/审计规则族
|
|
3141
|
+
// (docmost 全量告警 ~5% 全 FP:up()/down() 迁移函数被多族规则报)。
|
|
3142
|
+
// 与 node_modules 同款「无应用面」判定:迁移只在部署时跑一次,
|
|
3143
|
+
// 不暴露请求面。Python 侧 alembic(/migrations/ 或 /alembic/versions/)
|
|
3144
|
+
// 未在本轮测量,暂不动。
|
|
3145
|
+
if (sf.getFilePath().split(path.sep).join("/").includes("/migrations/"))
|
|
3146
|
+
continue;
|
|
3098
3147
|
const relPath = path.relative(absRoot, sf.getFilePath());
|
|
3099
3148
|
const _ft0 = Date.now();
|
|
3100
3149
|
for (const f of sf.getFunctions()) {
|
|
@@ -878,6 +878,23 @@ const SAFEGUARD_RULES = [
|
|
|
878
878
|
conceptMissing: ["RolePolicyOnTokenExchange", "DomainAllowlistOnTokenExchange"],
|
|
879
879
|
conceptExpected: ["get_user_role before session creation", "OAUTH_ALLOWED_DOMAINS check"],
|
|
880
880
|
},
|
|
881
|
+
{
|
|
882
|
+
name: "Authorization Identity Anchor from Request",
|
|
883
|
+
category: "auth",
|
|
884
|
+
// 2026-10-06(REALWORLD_FIX_REGRESSION fr-017):身份校验的锚点
|
|
885
|
+
// (token 属于哪个 app/client)从请求输入读取——tinacms 的 isAuthorized
|
|
886
|
+
// 从 req.query.clientID 取锚点去问身份服务「这 token 是不是该 app 的」,
|
|
887
|
+
// 攻击者注册自己的 app 拿合法 token 即通过任意站点的后端授权
|
|
888
|
+
// (GHSA-g74q-6g2f-874x)。修复 = 锚点改从服务端配置/环境取 +
|
|
889
|
+
// 取不到 fail-closed。判据在提取器侧 hasRequestAnchoredIdentity
|
|
890
|
+
// (三元组:请求取 token + 请求取 app/client 锚点 + 无服务端锚点证据)。
|
|
891
|
+
languages: ["typescript", "javascript"],
|
|
892
|
+
trigger: /\b(__progmune_request_anchored_identity__)\b/,
|
|
893
|
+
safeguards: [],
|
|
894
|
+
violationMessage: "Authorization validates a token against an app/client identity read from the request (query/body/searchParams) rather than server configuration — an attacker registers their own app identity, supplies a token that IS valid for it, and passes authorization on any endpoint using this function. Anchor the identity in server config/env and fail closed when it cannot be resolved.",
|
|
895
|
+
conceptMissing: ["ServerConfiguredIdentityAnchor"],
|
|
896
|
+
conceptExpected: ["expectedClientID from config/env", "fail-closed when anchor unresolved"],
|
|
897
|
+
},
|
|
881
898
|
{
|
|
882
899
|
name: "XSS (Unsafe Template Rendering)",
|
|
883
900
|
category: "xss",
|
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.62";
|
|
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.62",
|
|
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/",
|