progmune-runtime 3.7.60 → 3.7.61

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 CHANGED
@@ -1,6 +1,50 @@
1
1
  # Changelog
2
2
 
3
- ## [3.7.60] — 2026-10-05(未发布)
3
+ ## [3.7.61] — 2026-10-06(未发布)
4
+
5
+ ### fr-017 点亮:授权身份锚点取自请求输入(修复回归 11/17)
6
+
7
+ **归属语义漏报根因第一刀**:tinacms 的 `isAuthorized` 把「token 属于哪个
8
+ app/client」的身份锚点从 `req.query.clientID` 读取去问身份服务——攻击者
9
+ 注册自己的 app 拿合法 token,即通过任意站点的后端授权(confused-deputy
10
+ 变体,GHSA-g74q-6g2f-874x)。修复 = 锚点改从服务端配置取
11
+ (`expectedClientID ?? process.env.NEXT_PUBLIC_TINA_CLIENT_ID`)+ 取不到
12
+ fail-closed。
13
+
14
+ - 提取器 `src/extract-ir.ts`:`hasRequestAnchoredIdentity`——判据三元组
15
+ ① 从请求取 token(req.headers.authorization / headers().get('authorization'))
16
+ ② 从请求输入取 app/client 身份锚点(query/body/params 或 searchParams)
17
+ ③ 无服务端锚点证据(process.env. / getConfig( / config.)
18
+ ⇒ 注入 `__progmune_request_anchored_identity__`
19
+ - 规则 "Authorization Identity Anchor from Request"(ts/js,marker-driven,
20
+ 进 safeguard 告警流,与 fr-001/002/003 同族)
21
+ - 边界(写死在判据里):锚点名不含 tenant(多租户从请求解析租户锚点是
22
+ 合法形态);OAuth token 端点(req.body.client_id + client_secret,无
23
+ authorization header)不触发
24
+ - **三态验证**:修复前 2/2 真值函数命中(@tinacms/auth 与
25
+ next-tinacms-azure 两个变体)/ 修复后 0/0;TS 盲测 119 项目逐函数
26
+ 零漂移(LOST 0/ADDED 0);fp-pool 真实语料 9 切片 1186 函数 0 命中;
27
+ 定向测试 8/8。快照入库 `blind-benchmark/fr-corpus/fr-017-tinacms/`
28
+
29
+ ### fr-008 / fr-013 判定关闭(修复回归语料全量定性收官)
30
+
31
+ - **fr-008**(SimpleWebAuthn,密码学协议语义):证书链 walk 在第一个自签名
32
+ 证书处 break,永不检查链尾拼上的配置信任锚点——「链内部自洽」冒充
33
+ 「链终止于信任锚点」(修复 = break 条件改为 `issuer.equal(anchor)`)。
34
+ 检出需要建模证书链验证协议语义,任何标记/污点/缺席判据都够不着,
35
+ 归入 V1 七类根因「密码学协议语义」,不做。
36
+ - **fr-013**(gitlab-mcp,五子问题全部分类):F1(a) GraphQL 逗号前导形态
37
+ = 字符串解析语义;F1(b) execute_graphql 缺项目范围检查 = 缺席型判据
38
+ **唯一可点亮方向**,但判据是 MCP 工具处理器专属词汇,需 MCP 类公告
39
+ 语料族(≥2-3 条)支撑,单条按三要件不做;F2/F3 配置通道路由(启动校验
40
+ 门缺凭证形态 / SSE 默认无认证 + DNS rebinding);F4 会话耗尽 DoS;
41
+ F5 内容级提示注入防御。constant-time 比较属加固建议类不单做。
42
+ - **fr-corpus 17 条全部定性收官:有效 11/17(9 干净 DETECTED + 2
43
+ with-boundary),6 条判定关闭**(fr-004 闭包晚绑定 / fr-006 黑名单
44
+ 完备性 / fr-008 密码学协议语义 / fr-009 守卫不足 / fr-013 五子问题
45
+ 全出检出面 / fr-015 corpus_invalid+守卫不足)。
46
+
47
+ ## [3.7.60] — 2026-10-05
4
48
 
5
49
  ### fr-002/003 点亮:令牌交换路径检查完备性(修复回归 10/17)
6
50
 
@@ -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
+ });
@@ -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
  /**
@@ -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.60";
23
+ exports.RUNTIME_VERSION = "3.7.61";
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.60",
3
+ "version": "3.7.61",
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/",