T09 · Insecure Skill Coding Practices
- Location
engine.py:212- Finding
Unsanitized Report Parameters Permit Arbitrary File Writes
- Content
View full analysis
Vulnerability Details
File Location:
engine.py, lines 212-220
Vulnerability Type: Path traversal and unrestricted file write
Risk Level: HighVulnerable Code
python async def _generate_report(self) -> Path: """生成最终报告文件""" logger.info("📝 生成报告中...") now = datetime.now() timestamp = now.strftime('%Y-%m-%d %H:%M') filename = f"{self.topic.replace(' ', '_')}_{self.depth}_{now.strftime('%Y%m%d_%H%M')}.{self.output_format}" report_path = OUTPUT_DIR / filename # 渲染模板 template = self._load_template() content = self._render_report(template) report_path.write_text(content, encoding='utf-8')Technical Analysis
The
topic,depth, andoutput_formatvalues are obtained from attacker-controllable JSON supplied through standard input. These values are incorporated directly into the output filename without an allowlist, path-component sanitization, canonicalization, or containment check.Replacing spaces in
topicdoes not remove absolute path prefixes,..traversal components, or directory separators. In particular, whentopicbegins with/, the resultingfilenameis an absolute path. Python'spathlibdiscards the precedingOUTPUT_DIRwhen an absolute path is joined:python Path("/trusted/output") / "/tmp/report_standard_20260916_1200.markdown"This resolves to the attacker-selected
/tmp/...path rather than a location under/trusted/output. Relative traversal components can similarly escapeOUTPUT_DIRwhen the resulting parent directories exist.The report body also contains the attacker-controlled topic, so exploitation writes partially attacker-controlled content. The timestamp makes targeting an existing file less convenient, but it does not prevent unauthorized file creation outside the intended directory. Predictable timestamps and attacker-controlled suffix components can also increase overwrite opportunities.
...[truncated 1938 chars]
- Remediation
View remediation
Remediation Suggestions
-
Use a server-generated filename. Do not use user input as a filesystem path component. Generate a random identifier or digest and store the human-readable topic only inside the report.
python from uuid import uuid4 allowed_formats = {"markdown": "md", "html": "html", "json": "json"} extension = allowed_formats.get(self.output_format) if extension is None: raise ValueError("Unsupported output format") filename = f"research_{uuid4().hex}.{extension}" -
Strictly validate enumerated parameters. Allow only documented values for
depthandoutput_format:python if self.depth not in {"quick", "standard", "deep"}: raise ValueError("Invalid depth") if self.output_format not in {"markdown", "html", "json"}: raise ValueError("Invalid output format") -
If the topic must appear in the filename, convert it to a safe slug. Allow only a conservative set of characters, impose a length limit, and reject empty results. Do not merely remove spaces.
python import re slug = re.sub(r"[^A-Za-z0-9_-]+", "_", self.topic).strip("_")[:80] if not slug: slug = "research" -
Enforce canonical path containment before writing.
python base = OUTPUT_DIR.resolve() report_path = (base / filename).resolve() if report_path.parent != base: raise ValueError("Report path escapes the output directory") -
Prevent unintended overwrites. Use exclusive file creation where replacement is unnecessary:
python with report_path.open("x", encoding="utf-8") as handle: handle.write(content) -
Run the Skill with least privilege. Restrict the process account to the report directory and required memory interfaces. Avoid granting write access to application code, startup configuration, credentials, or shared executable directories.
...[truncated 191 chars]
-
