Back to skill

Security audit

PPT Presenter — 带逐字稿的演讲级PPT生成器

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a coherent presentation generator, but its image-generation helper can write generated image files outside the requested output folder if slide names are path-like.

Review this skill before installing if you will run the bundled image-generation script. Use only trusted prompts JSON files, keep slide names to simple filenames such as letters, numbers, dashes, and underscores, and use a restricted Gemini API key. Image prompts are sent to Google's API, and generated presentations load JavaScript, CSS, and fonts from external CDNs unless you modify the template.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate_slide_images.py:108
Finding
Path Traversal Through Unvalidated Slide Names<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate_slide_images.py`, lines 108-129 **Vulnerability Type**: Unrestricted filesystem path construction **Risk Level**: Medium ### Vulnerable Code ```python for i, slide in enumerate(slides): name = slide["name"] prompt = slide["prompt"] print(f"[{i + 1}/{total}] {name}...") result = None for attempt in range(1, args.retries + 1): result = generate_fn(prompt, args.api_key, args.model) if result: break if attempt < args.retries: wait = args.delay * attempt * 2 print(f" Retry {attempt}/{args.retries} in {wait}s...") time.sleep(wait) if result: img_data, ext = result outpath = os.path.join(args.output_dir, f"{name}.{ext}") with open(outpath, "wb") as f: f.write(img_data) ``` ### Technical Analysis The `name` value is read directly from the user-supplied prompts JSON file and incorporated into an output path without validation. The code does not reject absolute paths, directory separators, or parent-directory components such as `../`. For a relative traversal value, `os.path.join()` produces a path that can resolve outside `args.output_dir`. If `name` is absolute, `os.path.join()` discards the preceding output directory entirely. The script then opens the resulting path in `wb` mode, which creates the file or truncates an existing file. The file extension is derived from the API response, and the content is generated image data. These constraints reduce the likelihood of directly replacing an arbitrary extension-sensitive configuration file, but they do not prevent creation or overwriting of unintended files with an image extension. ### Attack Path 1. An attacker supplies or modifies the JSON file passed through `--prompts-file`. 2. The attacker sets a slide name to a traversal or absolute path, such as: ```json { "name": "../../shared/cover", ...[truncated 1233 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Treat each slide name as a filename rather than a path: 1. Reject empty names, absolute paths, `.` or `..` components, and all directory separators. 2. Apply a conservative allowlist, such as letters, digits, underscores, and hyphens. 3. Resolve both the output directory and candidate output path and verify that the candidate remains beneath the output directory. 4. Consider using exclusive creation mode if overwriting existing files is unnecessary. 5. Validate the prompts JSON schema before processing any slides. Example hardening: ```python import re from pathlib import Path SAFE_NAME = re.compile(r"^[A-Za-z0-9_-]{1,100}$") output_dir = Path(args.output_dir).resolve() output_dir.mkdir(parents=True, exist_ok=True) name = slide.get("name") if not isinstance(name, str) or not SAFE_NAME.fullmatch(name): raise ValueError(f"Invalid slide name: {name!r}") outpath = (output_dir / f"{name}.{ext}").resolve() if output_dir not in outpath.parents: raise ValueError("Output path escapes the requested output directory") with outpath.open("wb") as f: f.write(img_data) ``` The response-derived extension should also be checked against an explicit allowlist such as `{"png", "jpg", "jpeg", "webp"}`. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/generate_slide_images.py:31
Finding
Gemini API Credential Exposed in Request URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate_slide_images.py`, lines 31 and 60 **Vulnerability Type**: Sensitive credential included in URL query strings **Risk Level**: Low ### Vulnerable Code Gemini image-generation request: ```python def generate_with_gemini3(prompt: str, api_key: str, model: str = DEFAULT_MODEL) -> tuple[bytes, str] | None: """Generate image using Gemini 3 Pro Image (generateContent with IMAGE modality).""" url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}" payload = json.dumps({ "contents": [{"parts": [{"text": f"Generate this image: {prompt}"}]}], "generationConfig": {"responseModalities": ["IMAGE", "TEXT"]}, }).encode() req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) ``` Imagen request: ```python def generate_with_imagen(prompt: str, api_key: str, model: str = FALLBACK_MODEL) -> tuple[bytes, str] | None: """Generate image using Imagen 4.0 (predict endpoint).""" url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:predict?key={api_key}" payload = json.dumps({ "instances": [{"prompt": prompt}], "parameters": {"sampleCount": 1, "aspectRatio": "16:9"}, }).encode() req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) ``` ### Technical Analysis The Gemini API key is interpolated into the query string of every request. HTTPS protects the complete request target during transport to the declared Google endpoint, so the key is not sent in plaintext over the network. However, URL query strings are more likely than request headers to be captured by debugging tools, exception telemetry, HTTP proxy logs, monitoring systems, or other diagnostics. No code in the audited project explicitly logs the constructed URL, and the endpoint is consistent with the Skill’s documented image-generation fun ...[truncated 1766 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer an API-supported authentication header or Google’s official client library if the relevant endpoint supports that authentication mechanism. 2. If query-string authentication is required by the service, ensure that request URLs are never logged without redaction. 3. Configure proxies, telemetry systems, and exception handlers to replace the `key` query parameter with a fixed redaction marker. 4. Use API keys restricted to the required Google API, expected applications, and appropriate quotas. 5. Rotate any key suspected of appearing in logs. 6. Continue accepting the key through an environment variable or protected secret provider rather than hardcoding it. 7. Warn users that image prompts are transmitted to Google and should not contain confidential information unless external processing is intended. A redaction helper should be applied before any future diagnostic output: ```python from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit def redact_url(url: str) -> str: parts = urlsplit(url) query = [ (name, "REDACTED" if name.lower() == "key" else value) for name, value in parse_qsl(parts.query, keep_blank_values=True) ] return urlunsplit(( parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment, )) ``` ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
Findings (2)

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
The guidance says to match audience language and explicitly mandates Chinese for Chinese audiences. Under the policy, locale or language constraints should offer user choice or clear opt-in unless narrowly justified; this file does not state that the user can choose another language.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The HTML sets `lang="zh-CN"` and includes user-facing keyboard hints only in Chinese, which imposes a specific language/locale on all uses of the template. There is no visible opt-in, fallback, or documented justification that this template is intended only for a Chinese-language context.

Static analysis

No suspicious patterns detected.