progmune-runtime 3.7.27 → 3.7.28
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 +14 -0
- package/dist/extract-ir.js +91 -0
- package/dist/protocol-detector.js +11 -6
- package/dist/sdk.js +1 -1
- package/dist/trust/engine.js +14 -0
- package/package.json +1 -1
- package/tools/extract_ir.py +74 -21
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.7.28] — 2026-09-14
|
|
4
|
+
|
|
5
|
+
### SSRF 检测 TS 化 + Python 形参污点扩展(A3,修复回归 fr-009/010/011 驱动)
|
|
6
|
+
|
|
7
|
+
- **TS 提取器 SSRF 标记**(`extract-ir.ts`):URL 形参/request 污点 → HTTP fetch sink(fetch/axios.*/http.request/ky.*/undici.request/nodeFetch/got),函数内无 SSRF 守卫词汇(private-IP/loopback/denylist/hostname 校验)→ `__progmune_ssrf_user_url__`
|
|
8
|
+
- **Python 形参污点扩展**(`extract_ir.py`):URL 形参(url/target/endpoint/spec_url…)作为污点源(库形态,如 from_url(url));守卫抑制词汇(validate_url/safe_http/is_blocked_hostname 等;**不含裸 "ssrf"**——PyGoat 实验模板名 ssrf_lab2.html 会被误抑制);**is_request_rooted 收紧**:裸名 "request" 仅当它是函数形参才算请求根——此前局部变量命名为 request 会被误判(skyvern _fetch_discovery FP 根因)
|
|
9
|
+
- **引擎 IR 层消费**:SSRF 违规直接报出(同 PATH_TRAVERSAL/AUTHZ_CROSS_USER_WRITE 模式)——Python 侧 SSRF 标记此前在引擎管线中是死的,本版点亮
|
|
10
|
+
- **修复回归战绩 4/11**:fr-011(mcp-from-openapi TS,引擎级:修复前 SSRF ×1 @ fromURL 真值位置/修复后 0)✅、fr-010(unstructured Python,提取器级:修复前 2 标记命中 partition_md/file_and_type_from_url/修复后 0;整仓 tarball 网络截断,mini 语料验证如实记录)✅、PyGoat ssrf_lab2 引擎级 TP 恢复 ✅;fr-009(open-webui DNS 重绑定)为「守卫存在但不足」类,标记模型边界如实不覆盖
|
|
11
|
+
- **skyvern 精化**:_fetch_discovery FP 消除;_download_screenshot 为弱 TP(scheme 校验≠SSRF 防护,如实标注)
|
|
12
|
+
|
|
13
|
+
### 回归
|
|
14
|
+
|
|
15
|
+
- TS 795 零漂移(3086 LOST 0/ADDED 0)✓ / Python 盲测 64/64 ✓ / Python 源级金标 SSRF 0 命中 ✓ / open-seo 10 无漂移 ✓ / 引擎测试 73 green ✓ / build ✓ / check 免疫正常 ✓
|
|
16
|
+
|
|
3
17
|
## [3.7.27] — 2026-09-13
|
|
4
18
|
|
|
5
19
|
### 归属校验 Python 化(A2,修复回归 fr-005 驱动)
|
package/dist/extract-ir.js
CHANGED
|
@@ -598,6 +598,96 @@ function augmentPathTraversalMarkers(project, funcs, absRoot) {
|
|
|
598
598
|
});
|
|
599
599
|
}
|
|
600
600
|
}
|
|
601
|
+
// ═══════════════════════════════════════════════════════════════
|
|
602
|
+
// SSRF marker(2026-09-14,REALWORLD_FIX_REGRESSION fr-011/fr-010)
|
|
603
|
+
// URL 形参/request 污点 → HTTP fetch sink,函数内无 SSRF 守卫证据
|
|
604
|
+
// (private-IP/loopback/denylist/hostname 校验词汇)→ 注入
|
|
605
|
+
// __progmune_ssrf_user_url__。库形态(mcp-from-openapi fromURL)与
|
|
606
|
+
// Web 形态(req.params)共用同一标记;引擎 IR 层消费。
|
|
607
|
+
// ═══════════════════════════════════════════════════════════════
|
|
608
|
+
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*\(/;
|
|
609
|
+
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;
|
|
610
|
+
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;
|
|
611
|
+
function collectUrlParamNames(params) {
|
|
612
|
+
return params.map((p) => p.name).filter((n) => URL_PARAM_NAME.test(n));
|
|
613
|
+
}
|
|
614
|
+
function augmentSsfrMarkers(project, funcs, absRoot) {
|
|
615
|
+
const sinkRe = TS_HTTP_FETCH_SINK;
|
|
616
|
+
for (const sf of project.getSourceFiles()) {
|
|
617
|
+
if (sf.getFilePath().includes("node_modules"))
|
|
618
|
+
continue;
|
|
619
|
+
const relPath = path.relative(absRoot, sf.getFilePath());
|
|
620
|
+
sf.forEachDescendant((node) => {
|
|
621
|
+
if (!ts_morph_1.Node.isFunctionDeclaration(node) &&
|
|
622
|
+
!ts_morph_1.Node.isArrowFunction(node) &&
|
|
623
|
+
!ts_morph_1.Node.isMethodDeclaration(node)) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const text = node.getText();
|
|
627
|
+
if (text.length > 12000)
|
|
628
|
+
return; // 巨型函数跳过(性能 + 噪声)
|
|
629
|
+
// 预过滤:必须有 fetch sink
|
|
630
|
+
sinkRe.lastIndex = 0;
|
|
631
|
+
if (!sinkRe.test(text))
|
|
632
|
+
return;
|
|
633
|
+
// 守卫证据:函数内有 SSRF 防护词汇 → 抑制
|
|
634
|
+
if (SSRF_GUARD_EVIDENCE.test(text))
|
|
635
|
+
return;
|
|
636
|
+
// URL 形参污点
|
|
637
|
+
const urlParams = ts_morph_1.Node.isArrowFunction(node)
|
|
638
|
+
? []
|
|
639
|
+
: collectUrlParamNames(node.getParameters().map((p) => ({ name: p.getName() })));
|
|
640
|
+
// request 污点(Web 形态)
|
|
641
|
+
const reqTainted = collectTaintedNames(text);
|
|
642
|
+
const taintParts = [];
|
|
643
|
+
if (urlParams.length > 0)
|
|
644
|
+
taintParts.push(`\\b(?:${urlParams.join("|")})\\b`);
|
|
645
|
+
if (reqTainted.size > 0)
|
|
646
|
+
taintParts.push(`\\b(?:${[...reqTainted].join("|")})\\b`);
|
|
647
|
+
taintParts.push(/\b(?:req|request)\.(?:params|query|body|headers|cookies)\b[.[]?/.source);
|
|
648
|
+
const taint = new RegExp(taintParts.join("|"));
|
|
649
|
+
if (taintParts.length <= 1) {
|
|
650
|
+
// 只有 request 根模式而无实际污点名——仍需检查 sink 实参
|
|
651
|
+
}
|
|
652
|
+
// sink 实参窗口含污点 → 标记
|
|
653
|
+
let flagged = false;
|
|
654
|
+
sinkRe.lastIndex = 0;
|
|
655
|
+
let m;
|
|
656
|
+
while ((m = sinkRe.exec(text)) !== null) {
|
|
657
|
+
// 取调用后 300 字符窗口(URL 是首个实参)
|
|
658
|
+
const after = text.slice(m.index + m[0].length, m.index + m[0].length + 300);
|
|
659
|
+
if (taint.test(after)) {
|
|
660
|
+
flagged = true;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (!flagged)
|
|
665
|
+
return;
|
|
666
|
+
const nodeName = ts_morph_1.Node.isArrowFunction(node) ? undefined : node.getName();
|
|
667
|
+
if (ts_morph_1.Node.isFunctionDeclaration(node) && nodeName) {
|
|
668
|
+
const entry = funcs.find((x) => x.name === nodeName && x.file === relPath);
|
|
669
|
+
if (entry) {
|
|
670
|
+
entry.calls = entry.calls || [];
|
|
671
|
+
if (!entry.calls.includes("__progmune_ssrf_user_url__")) {
|
|
672
|
+
entry.calls.push("__progmune_ssrf_user_url__");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else if (ts_morph_1.Node.isMethodDeclaration(node) && nodeName) {
|
|
677
|
+
const cls = node.getParent();
|
|
678
|
+
if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName()) {
|
|
679
|
+
const entry = funcs.find((x) => x.name === `${cls.getName()}.${nodeName}` && x.file === relPath);
|
|
680
|
+
if (entry) {
|
|
681
|
+
entry.calls = entry.calls || [];
|
|
682
|
+
if (!entry.calls.includes("__progmune_ssrf_user_url__")) {
|
|
683
|
+
entry.calls.push("__progmune_ssrf_user_url__");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
601
691
|
/**
|
|
602
692
|
* 从 TypeScript 项目提取 IR(函数签名、参数、返回值、协议注解)。
|
|
603
693
|
* @protocol namespace=dev_pipeline pre_states=[] post_states=["IR_EXTRACTED"] invalidate=["IR_STALE"]
|
|
@@ -784,6 +874,7 @@ function _extractSingleProject(absRoot, tsconfigPath) {
|
|
|
784
874
|
// 与 protocol-detector 同名规则(source-level benchmark 路径)。
|
|
785
875
|
// ═══════════════════════════════════════════════════════════════
|
|
786
876
|
augmentPathTraversalMarkers(project, funcs, absRoot);
|
|
877
|
+
augmentSsfrMarkers(project, funcs, absRoot);
|
|
787
878
|
// ═══════════════════════════════════════════════════════════════
|
|
788
879
|
// Phase 5: Dynamic external function resolution
|
|
789
880
|
// Replaces hardcoded knownExternals
|
|
@@ -658,12 +658,17 @@ const SAFEGUARD_RULES = [
|
|
|
658
658
|
{
|
|
659
659
|
name: "SSRF (User-Controlled URL Fetch)",
|
|
660
660
|
category: "ssrf",
|
|
661
|
-
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
//
|
|
666
|
-
//
|
|
661
|
+
// 2026-09-14(A3 fr-011/fr-010):TS 提取器(extract-ir.ts)自 2026-09-14
|
|
662
|
+
// 起同样注入该标记(URL 形参/request 污点 → HTTP fetch sink,无 SSRF
|
|
663
|
+
// 守卫),Python 侧同版扩展形参污点——语言面扩展。
|
|
664
|
+
languages: ["typescript", "javascript", "python"],
|
|
665
|
+
// Source-level detection: the language extractors emit a synthetic marker
|
|
666
|
+
// call when an HTTP fetch (requests.*/urllib.*/httpx.*/aiohttp.*/urlopen;
|
|
667
|
+
// TS: fetch/axios.*/http.request/ky.*/got/undici.request) receives a URL
|
|
668
|
+
// tainted by request-derived user input or a URL-shaped function parameter
|
|
669
|
+
// (directly or via single-hop assignment) with no SSRF guard in the
|
|
670
|
+
// function (private-IP/loopback/denylist/hostname validation evidence).
|
|
671
|
+
// No satisfier possible — the marker IS the violation evidence.
|
|
667
672
|
trigger: /\b(__progmune_ssrf_user_url__)\b/,
|
|
668
673
|
safeguards: [],
|
|
669
674
|
violationMessage: "HTTP fetch whose URL derives from user-controlled request input — server-side request forgery.",
|
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.28";
|
|
24
24
|
function verify(filePath) {
|
|
25
25
|
const cert = (0, certify_1.certify)(filePath);
|
|
26
26
|
const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
|
package/dist/trust/engine.js
CHANGED
|
@@ -1282,6 +1282,20 @@ async function collectProtocolViolations(ctx, callGraph) {
|
|
|
1282
1282
|
policy_ref: "protocol-safety.specific",
|
|
1283
1283
|
});
|
|
1284
1284
|
}
|
|
1285
|
+
if (calls.includes("__progmune_ssrf_user_url__")) {
|
|
1286
|
+
violations.push({
|
|
1287
|
+
severity: "medium",
|
|
1288
|
+
rule_id: "SSRF",
|
|
1289
|
+
file: f.file,
|
|
1290
|
+
function: f.name || "unknown",
|
|
1291
|
+
message: "HTTP fetch whose URL derives from user-controlled input or a URL-shaped function parameter with no SSRF guard — server-side request forgery.",
|
|
1292
|
+
evidence: "__progmune_ssrf_user_url__",
|
|
1293
|
+
why: "Extractor-verified: fetch sink receives request-tainted or URL-parameter input " +
|
|
1294
|
+
"with no private-IP/loopback/denylist/hostname validation evidence in the function.",
|
|
1295
|
+
fix: "Validate the URL (scheme allowlist, private-IP/loopback block, DNS pinning) before fetching.",
|
|
1296
|
+
policy_ref: "protocol-safety.specific",
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1285
1299
|
if (calls.includes("__progmune_cross_user_write__")) {
|
|
1286
1300
|
violations.push({
|
|
1287
1301
|
severity: "medium",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "progmune-runtime",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.28",
|
|
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/",
|
package/tools/extract_ir.py
CHANGED
|
@@ -105,40 +105,50 @@ SSRF_MARKER = "__progmune_ssrf_user_url__"
|
|
|
105
105
|
HTTP_FETCH_RECEIVERS = ("requests", "httpx", "urllib", "urllib2", "aiohttp", "http")
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def is_request_rooted(node):
|
|
108
|
+
def is_request_rooted(node, request_params=None):
|
|
109
109
|
"""True if the expression derives from the request object
|
|
110
|
-
(request.POST.get(...), request.GET['x'], request.body ...).
|
|
110
|
+
(request.POST.get(...), request.GET['x'], request.body ...).
|
|
111
|
+
2026-09-14 收紧:裸名 "request" 仅当它是函数形参(Web 处理器签名)
|
|
112
|
+
时才算 request 根——此前任何局部变量命名为 request 都被误判
|
|
113
|
+
(skyvern _fetch_discovery:request = urllib.request.Request(url),
|
|
114
|
+
局部变量撞词 → SSRF 误报)。request_params=None 保持旧行为。"""
|
|
111
115
|
if isinstance(node, ast.Name):
|
|
112
|
-
|
|
116
|
+
if node.id != "request":
|
|
117
|
+
return False
|
|
118
|
+
return request_params is None or "request" in request_params
|
|
113
119
|
if isinstance(node, ast.Attribute):
|
|
114
|
-
return is_request_rooted(node.value)
|
|
120
|
+
return is_request_rooted(node.value, request_params)
|
|
115
121
|
if isinstance(node, ast.Subscript):
|
|
116
|
-
return is_request_rooted(node.value)
|
|
122
|
+
return is_request_rooted(node.value, request_params)
|
|
117
123
|
if isinstance(node, ast.Call):
|
|
118
|
-
return (is_request_rooted(node.func)
|
|
119
|
-
or any(is_request_rooted(a) for a in node.args)
|
|
120
|
-
or any(is_request_rooted(k.value) for k in node.keywords))
|
|
124
|
+
return (is_request_rooted(node.func, request_params)
|
|
125
|
+
or any(is_request_rooted(a, request_params) for a in node.args)
|
|
126
|
+
or any(is_request_rooted(k.value, request_params) for k in node.keywords))
|
|
121
127
|
return False
|
|
122
128
|
|
|
123
129
|
|
|
124
|
-
def
|
|
130
|
+
def _func_param_names(node):
|
|
131
|
+
return {a.arg for a in node.args.args + node.args.posonlyargs + node.args.kwonlyargs}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_tainted(node, assigns, depth=0, request_params=None):
|
|
125
135
|
"""Single-hop taint: request-rooted, variable assigned from a tainted
|
|
126
136
|
value, or dynamic formatting containing tainted parts."""
|
|
127
137
|
if node is None or depth > 2:
|
|
128
138
|
return False
|
|
129
|
-
if is_request_rooted(node):
|
|
139
|
+
if is_request_rooted(node, request_params):
|
|
130
140
|
return True
|
|
131
141
|
if isinstance(node, ast.Name):
|
|
132
|
-
return is_tainted(assigns.get(node.id), assigns, depth + 1)
|
|
142
|
+
return is_tainted(assigns.get(node.id), assigns, depth + 1, request_params)
|
|
133
143
|
if isinstance(node, ast.JoinedStr):
|
|
134
|
-
return any(is_tainted(v.value, assigns, depth + 1)
|
|
144
|
+
return any(is_tainted(v.value, assigns, depth + 1, request_params)
|
|
135
145
|
for v in node.values if isinstance(v, ast.FormattedValue))
|
|
136
146
|
if isinstance(node, ast.BinOp):
|
|
137
|
-
return (is_tainted(node.left, assigns, depth)
|
|
138
|
-
or is_tainted(node.right, assigns, depth))
|
|
147
|
+
return (is_tainted(node.left, assigns, depth, request_params)
|
|
148
|
+
or is_tainted(node.right, assigns, depth, request_params))
|
|
139
149
|
if isinstance(node, ast.Call):
|
|
140
|
-
return (any(is_tainted(a, assigns, depth) for a in node.args)
|
|
141
|
-
or any(is_tainted(k.value, assigns, depth) for k in node.keywords))
|
|
150
|
+
return (any(is_tainted(a, assigns, depth, request_params) for a in node.args)
|
|
151
|
+
or any(is_tainted(k.value, assigns, depth, request_params) for k in node.keywords))
|
|
142
152
|
return False
|
|
143
153
|
|
|
144
154
|
|
|
@@ -162,15 +172,57 @@ def is_http_fetch_call(node):
|
|
|
162
172
|
return False
|
|
163
173
|
|
|
164
174
|
|
|
175
|
+
URL_PARAM_NAMES = re.compile(
|
|
176
|
+
r"^(url|target|endpoint|link|href|web_url|source_url|remote_url|"
|
|
177
|
+
r"fetch_url|spec_url|api_url|base_url|origin)$",
|
|
178
|
+
re.I,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
SSRF_GUARD_EVIDENCE = re.compile(
|
|
182
|
+
r"127\.0\.0\.1|0\.0\.0\.0|169\.254\.|::1|localhost|is_private|"
|
|
183
|
+
r"is_loopback|hostname|deny_?list|block_?list|validate_url|"
|
|
184
|
+
r"is_safe_url|get_addresses|ipaddress|forbidden_host|private_ip|"
|
|
185
|
+
r"safe_http|get_ssrf_safe|assert_url_safe|is_blocked_hostname|"
|
|
186
|
+
r"is_blocked_address|blocked_hostnames",
|
|
187
|
+
re.I,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
165
191
|
def has_ssrf(node):
|
|
166
192
|
"""SSRF check: an HTTP fetch call whose URL argument is tainted by
|
|
167
|
-
request-derived user input (directly or via single-hop assignment)
|
|
193
|
+
request-derived user input (directly or via single-hop assignment)
|
|
194
|
+
or references a URL-shaped function parameter (library form, e.g.
|
|
195
|
+
from_url(url)) — with no SSRF guard evidence in the function
|
|
196
|
+
(private-IP/loopback/denylist/hostname validation vocabulary).
|
|
197
|
+
2026-09-14 扩展(fr-010/fr-011):形参污点 + 守卫抑制。"""
|
|
198
|
+
# 守卫证据:函数体内有 SSRF 防护词汇 → 不报(fr-009 证明
|
|
199
|
+
# 「守卫存在但不足」的 DNS 重绑定类是标记模型的边界,如实不覆盖)
|
|
200
|
+
try:
|
|
201
|
+
text = ast.unparse(node)
|
|
202
|
+
except Exception:
|
|
203
|
+
text = ""
|
|
204
|
+
if text and SSRF_GUARD_EVIDENCE.search(text):
|
|
205
|
+
return False
|
|
168
206
|
assigns = collect_assignments(node)
|
|
207
|
+
request_params = _func_param_names(node)
|
|
208
|
+
url_params = {
|
|
209
|
+
a for a in request_params if URL_PARAM_NAMES.match(a or "")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
def refs_url_param(n):
|
|
213
|
+
if isinstance(n, ast.Name):
|
|
214
|
+
return n.id in url_params
|
|
215
|
+
if isinstance(n, (ast.Attribute, ast.Subscript)):
|
|
216
|
+
return refs_url_param(n.value)
|
|
217
|
+
return False
|
|
218
|
+
|
|
169
219
|
for child in ast.walk(node):
|
|
170
220
|
if isinstance(child, ast.Call) and is_http_fetch_call(child):
|
|
171
|
-
if any(is_tainted(a, assigns) for a in child.args):
|
|
221
|
+
if any(is_tainted(a, assigns, request_params=request_params) for a in child.args):
|
|
222
|
+
return True
|
|
223
|
+
if any(is_tainted(k.value, assigns, request_params=request_params) for k in child.keywords):
|
|
172
224
|
return True
|
|
173
|
-
if any(
|
|
225
|
+
if any(refs_url_param(a) for a in child.args):
|
|
174
226
|
return True
|
|
175
227
|
return False
|
|
176
228
|
|
|
@@ -827,12 +879,13 @@ def has_path_traversal(node):
|
|
|
827
879
|
request-derived user input (directly or via single-hop assignment —
|
|
828
880
|
os.path.join chains resolve through assignment tracking)."""
|
|
829
881
|
assigns = collect_assignments(node)
|
|
882
|
+
request_params = _func_param_names(node)
|
|
830
883
|
for child in ast.walk(node):
|
|
831
884
|
if not isinstance(child, ast.Call) or not is_file_sink_call(child):
|
|
832
885
|
continue
|
|
833
|
-
if any(is_tainted(a, assigns) for a in child.args):
|
|
886
|
+
if any(is_tainted(a, assigns, request_params=request_params) for a in child.args):
|
|
834
887
|
return True
|
|
835
|
-
if any(is_tainted(k.value, assigns) for k in child.keywords):
|
|
888
|
+
if any(is_tainted(k.value, assigns, request_params=request_params) for k in child.keywords):
|
|
836
889
|
return True
|
|
837
890
|
# Path(...).read_text(): the tainted path lives in the receiver —
|
|
838
891
|
# either the direct call or a variable assigned from a Path(...) call.
|