Back to skill

Security audit

公众号写稿 & 改写润色(AI)

Security checks for vulnerabilities and agentic risk

Overview

This writing skill is mostly coherent, but it can send sensitive drafts and API keys to arbitrary configured model endpoints and has a preset path bug that can read Markdown files outside the intended preset folders.

Review `article.yaml` and any draft package before running the skill, especially `default_structure` and `default_closing_block`. Use a dedicated low-quota API key, configure only trusted HTTPS model endpoints, and avoid passing confidential reference documents unless you are comfortable sending their full text to that provider.

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/write.py:263
Finding

Preset path traversal can disclose arbitrary local Markdown files

Content
View full analysis
Path | None: for root in preset_dirs: d = root / subdir if not d.exists(): continue for ext in exts: p = d / f"{name}{ext}" if p.exists(): return p return None def _load_closing_block(screening: dict, article_cfg: dict) -> str: """ Closing block: preset selection reads default_closing_block from article.yaml. If no preset is selected, use the inline closing_block from merged context. """ default_name = _coerce_single_preset( "default_closing_block", article_cfg.get("default_closing_block"), ) if default_name: preset_dirs = _preset_dirs(_aws_root()) found = _find_preset_file( preset_dirs, "closing-blocks", default_name, [".md"], ) if found: _info(f"Loading closing block preset: {found}") return found.read_text(encoding="utf-8") return (screening.get("closing_block") or "").strip() ``` The same unsafe lookup is also used by `_load_structure_template()` for `default_structure`. ### Technical Analysis Preset names read from the article's `article.yaml` are appended directly to a preset directory: ```python p = d / f"{name}{ext}" ``` The code does not reject: - `..` path components; - forward or backward path separators; - absolute paths; - paths that resolve outside the intended preset directory. It also does not resolve the resulting candidate and verify that the candidate remains a descendant of `presets/structures` or `presets/closing-blocks`. For example, a preset value conceptually equivalent to ...[truncated 2006 chars]
Remediation
View remediation
Path | None: if ( not name or Path(name).is_absolute() or ".." in Path(name).parts or "/" in name or "\\" in name or not _SAFE_PRESET_NAME.fullmatch(name) ): _err("Preset name contains prohibited path characters") root = preset_dir.resolve() candidate = (root / f"{name}{ext}").resolve() try: candidate.relative_to(root) except ValueError: _err("Preset path escapes the permitted preset directory") return candidate if candidate.is_file() else None ``` Where supported, `candidate.is_relative_to(root)` can be used instead of the `relative_to()` exception pattern. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/write.py:314
Finding

Unrestricted API endpoint permits plaintext transmission of credentials and article content

Content
View full analysis
str: """ Protocol detection priority: 1) Explicit provider, if configured 2) Automatic detection from base_url path characteristics """ p = (model_cfg.get("provider") or "").strip().lower() allowed = {"openai", "volcengine", "qwen", "gemini"} if p: if p not in allowed: _err( f"Unrecognized writing_model.provider: {p}; " "use openai | volcengine | qwen | gemini" ) return p base_url = (model_cfg.get("base_url") or "").strip().lower() if "/v1beta/models/" in base_url and ":generatecontent" in base_url: return "gemini" if ( "dashscope.aliyuncs.com" in base_url and "/compatible-mode/v1/chat/completions" in base_url ): return "qwen" if ( "volces.com" in base_url and "ark." in base_url and "/api/v3/chat/completions" in base_url ): return "volcengine" if "/v1/chat/completions" in base_url: return "openai" _err( "Unable to detect protocol type from writing_model.base_url/model." ) def _post_json( url: str, body: dict, api_key: str, timeout: int = 300, ) -> dict: data = json.dumps(body, ensure_ascii=False).encode("utf-8") req = urllib.request.Request( url, data=data, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", "User-Agent": "aws-article-writer/1.0", }, ) try: with urllib.request.urlopen(req, timeout=timeout) as resp: return json.loads(resp.read()) except urllib.error.HTTPError as e: error_body = e.read().dec ...[truncated 3119 chars]
Remediation
View remediation
None: parsed = urlsplit(url) if parsed.scheme != "https": _err("Model endpoint must use HTTPS") if not parsed.hostname: _err("Model endpoint has no valid hostname") if parsed.username or parsed.password: _err("Credentials must not be embedded in the model endpoint URL") for result in socket.getaddrinfo(parsed.hostname, parsed.port or 443): address = ipaddress.ip_address(result[4][0]) prohibited = ( address.is_loopback or address.is_link_local or address.is_multicas ...[truncated 376 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill explicitly documents sensitive capabilities including reading local files, writing drafts, and sending article content plus an API key to a user-configured external endpoint, but it does not declare any explicit tool scope or allowed-tools boundary. That mismatch weakens least-privilege controls and makes misuse or overreach harder to constrain or audit, especially because the skill can access credentials and exfiltrate repository content over the network.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The documentation states that full local reference documents are injected into the model prompt, but it does not clearly warn that this content will be transmitted to a configured external OpenAI-compatible endpoint. In this skill context, users may supply internal product documents under .aws-article/products/..., so omission of an explicit data egress warning creates a real confidentiality risk, especially when using third-party or proxy model providers.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The script sends full article text, user instructions, image analysis, and optionally entire reference documents to third-party LLM endpoints via _post_json without an explicit runtime consent gate or warning at the transmission point. Because base_url is configurable and may target arbitrary external providers or gateways, users can unintentionally exfiltrate proprietary drafts, internal product materials, or sensitive business content outside the local environment.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The manifest description is written exclusively in Chinese and describes the skill as writing WeChat public-account articles, which implies a fixed language/locale orientation without any opt-in or alternative. Under the policy rule, language or locale constraints should either offer user choice or be explicitly justified as region-specific.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The manifest grants shell access to a skill whose stated purpose is article writing and rewriting, and the manifest provides no functional justification for executing local commands. Unnecessary shell capability materially expands the attack surface: prompt-influenced instructions or downstream skill logic could invoke arbitrary commands, access local data, or chain with filesystem/network permissions for exfiltration or system modification.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

Line L092 states that image descriptions must be written in Chinese. This is a natural-language locale requirement, and the document does not provide user opt-in or explain that the skill is intentionally limited to a Chinese-only regional context.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
81% confidence
Finding

SQP-3 适用于所有文件类型。该文件整体以中文规定使用方式与输出场景,但未说明这是可选语言还是仅适用于特定中文团队/区域,存在未获用户选择即固定语言/locale 的组织政策风险。

Content

No source excerpt is available for this finding.