Back to skill

Security audit

内容引擎 / Content Engine

Security checks for vulnerabilities and agentic risk

Overview

The skill matches its content-publishing purpose, but its Obsidian import/export code can read or overwrite files outside the intended vault, so it needs review before installation.

Install only after reviewing the Obsidian path handling or disabling those workflows. Use least-privilege platform tokens, keep secrets out of logs and shell history where possible, and require an explicit preview plus confirmation before any public publish, schedule, delete, export, or file overwrite action.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/obsidian_sync.py:440
Finding
Arbitrary File Read Through Obsidian Import Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/obsidian_sync.py:440-460` **Vulnerability Type**: Path traversal leading to arbitrary local file read **Risk Level**: High ### Vulnerable Code ```python file_rel = data.get("file", "") if not file_rel: output_error("笔记文件路径(file)为必填字段", code="VALIDATION_ERROR") return vault_path = data.get("vault_path") or _get_vault_path() if not vault_path: state = _get_sync_state() vault_path = state.get("vault_path") if not vault_path or not os.path.isdir(vault_path): output_error("未连接到 Obsidian 笔记库,请先执行 connect 操作", code="NOT_CONNECTED") return fpath = os.path.join(vault_path, file_rel) if not os.path.exists(fpath): output_error(f"笔记文件不存在: {file_rel}", code="FILE_NOT_FOUND") return try: with open(fpath, "r", encoding="utf-8") as f: raw_content = f.read() ``` ### Technical Analysis The `import_draft` operation describes `file` as a path relative to the configured Obsidian vault, but the implementation does not enforce that restriction. `os.path.join(vault_path, file_rel)` does not provide path containment. A value containing `../` components can resolve outside the vault. If `file_rel` is an absolute path, `os.path.join` discards `vault_path` entirely. The implementation also does not resolve symlinks, so a symlink located inside the vault can point to a file outside it. The subsequent existence check only establishes that the resulting path exists. It does not verify that the resolved path remains under the configured vault root. ### Attack Path 1. The attacker or untrusted caller invokes the `import-draft` action. 2. The supplied JSON sets `file` to a traversal or absolute path, such as: ```json { "file": "../../sensitive-file.md" } ``` 3. The code joins the crafted value with the vault path without normalization or containment validation. 4. `open()` reads the external file using the privileges of the Agent process. 5. The file contents ...[truncated 652 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Create a shared helper that resolves and validates all vault paths before any filesystem access: ```python def resolve_vault_path(vault_path: str, relative_path: str) -> str: if not relative_path or os.path.isabs(relative_path): raise ValueError("The file path must be relative to the vault") vault = os.path.realpath(os.path.expanduser(vault_path)) target = os.path.realpath(os.path.join(vault, relative_path)) try: inside_vault = os.path.commonpath([vault, target]) == vault except ValueError: inside_vault = False if not inside_vault: raise ValueError("The file path must remain inside the vault") return target ``` Use this helper instead of directly calling `os.path.join`. In addition: 1. Reject absolute paths and traversal outside the vault. 2. Require the imported target to be a regular `.md` file. 3. Resolve symlinks before checking containment. 4. Consider rejecting symlink targets entirely when imports are expected to operate only on ordinary vault files. 5. Avoid returning unnecessary external path information in error messages. 6. Add tests for absolute paths, `../` traversal, nested traversal, symlink escapes, and valid nested vault notes. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/obsidian_sync.py:566
Finding
Arbitrary File Write Through Obsidian Export Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/obsidian_sync.py:566-603` **Vulnerability Type**: Path traversal leading to arbitrary local file creation or overwrite **Risk Level**: High ### Vulnerable Code ```python # 确定目标文件路径 file_rel = data.get("file", "") if not file_rel: # 自动生成文件名 safe_title = re.sub(r"[^\w\u4e00-\u9fff-]", "-", title) safe_title = re.sub(r"-+", "-", safe_title).strip("-") file_rel = f"{safe_title}.md" fpath = os.path.join(vault_path, file_rel) # 构建 frontmatter fm_data = {"title": title} if data.get("tags"): tags = data["tags"] if isinstance(tags, str): tags = [t.strip() for t in tags.split(",") if t.strip()] fm_data["tags"] = tags if data.get("platforms"): platforms = data["platforms"] if isinstance(platforms, str): platforms = [p.strip() for p in platforms.split(",") if p.strip()] fm_data["platforms"] = platforms if data.get("author"): fm_data["author"] = data["author"] if data.get("summary"): fm_data["summary"] = data["summary"] if data.get("ce_id"): fm_data["ce_id"] = data["ce_id"] if data.get("ce_status"): fm_data["ce_status"] = data["ce_status"] if data.get("ce_published_at"): fm_data["ce_published_at"] = data["ce_published_at"] frontmatter = _build_frontmatter(fm_data) full_content = frontmatter + "\n\n" + body # 写入文件 try: os.makedirs(os.path.dirname(fpath) if os.path.dirname(fpath) != "" else fpath, exist_ok=True) with open(fpath, "w", encoding="utf-8") as f: f.write(full_content) ``` ### Technical Analysis When the caller does not supply `file`, the generated filename is sanitized. However, when `file` is explicitly supplied, it is used without validation. A traversal path such as `../../target.md` escapes the vault. An absolute path causes `os.path.join` to ignore the vault path. The code then creates parent directories and opens the target in write mode, which truncates an existing file before writing attacker-c ...[truncated 1481 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Resolve the destination canonically and require it to remain inside the configured vault: ```python vault = os.path.realpath(os.path.expanduser(vault_path)) if os.path.isabs(file_rel): raise ValueError("The destination must be relative to the vault") target = os.path.realpath(os.path.join(vault, file_rel)) if os.path.commonpath([vault, target]) != vault: raise ValueError("The destination must remain inside the vault") ``` Additional hardening should include: 1. Permit only `.md` destinations where feasible. 2. Reject empty basenames, `.` components, and parent traversal. 3. Reject symlink destination components or use no-follow filesystem operations where supported. 4. Create files atomically with restrictive permissions. 5. Avoid overwriting existing notes unless the user explicitly confirms the overwrite. 6. Create only the validated parent directory, not a path derived from an unchecked value. 7. Add regression tests covering absolute paths, `../` escapes, symlink escapes, existing-file truncation, and valid nested exports. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/content_store.py:591
Finding
Arbitrary File Read Through Content Store Obsidian Import<![CDATA[ ## Vulnerability Details **File Location**: `scripts/content_store.py:591-613` **Vulnerability Type**: Path traversal leading to arbitrary local file read **Risk Level**: High ### Vulnerable Code ```python vault_path = data.get("vault_path") or os.environ.get("CE_OBSIDIAN_VAULT_PATH", "") if vault_path: vault_path = os.path.expanduser(vault_path) if not vault_path: state = _obsidian_sync._get_sync_state() vault_path = state.get("vault_path", "") if not vault_path or not os.path.isdir(vault_path): output_error( "未连接到 Obsidian 笔记库,请先设置 CE_OBSIDIAN_VAULT_PATH 或执行 obsidian_sync connect", code="NOT_CONNECTED", ) return fpath = os.path.join(vault_path, file_rel) if not os.path.exists(fpath): output_error(f"笔记文件不存在: {file_rel}", code="FILE_NOT_FOUND") return try: with open(fpath, "r", encoding="utf-8") as f: raw_content = f.read() ``` ### Technical Analysis The content-store Obsidian import duplicates the unsafe path construction found in the dedicated synchronization module. It joins the configured vault with caller-controlled `file_rel` but does not reject absolute paths, normalize traversal components, verify canonical containment, or address symlink escapes. Checking that `vault_path` is a directory does not constrain the final `fpath`. Checking that `fpath` exists likewise provides no security boundary. ### Attack Path 1. The attacker or untrusted caller invokes the `import-obsidian` action. 2. The request supplies an absolute path or a traversal value such as `../../external.md`. 3. The path resolves outside the configured Obsidian vault. 4. The code reads the external file with the process’s privileges. 5. The imported content is parsed and incorporated into the local content workflow and command output. ### Impact Assessment This flaw permits disclosure of arbitrary readable UTF-8 text files outside the intended vault. The exposed data can then enter the content store or be re ...[truncated 213 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Reuse a single audited vault-path resolver across `content_store.py` and `obsidian_sync.py` rather than independently constructing paths. The resolver should: 1. Expand and canonicalize the vault root. 2. reject absolute caller-supplied file paths. 3. Canonicalize the resulting target. 4. Compare the target and vault with `os.path.commonpath`. 5. Reject targets outside the vault. 6. Require a regular Markdown file. 7. Resolve or reject symlinks before reading. 8. Return a generic validation error without exposing sensitive external paths. Example: ```python vault = os.path.realpath(os.path.expanduser(vault_path)) target = os.path.realpath(os.path.join(vault, file_rel)) if os.path.isabs(file_rel) or os.path.commonpath([vault, target]) != vault: output_error( "The note path must remain inside the configured vault", code="INVALID_PATH", ) return if not os.path.isfile(target) or not target.lower().endswith(".md"): output_error("The source must be a Markdown file", code="INVALID_FILE") return ``` ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (68)

Tainted flow: 'req' from os.environ.get (line 290, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
f"grant_type=client_credential&appid={appid}&secret={secret}"
        )
        req = Request(token_url, method="GET")
        with urlopen(req, timeout=30) as resp:
            token_data = json.loads(resp.read().decode("utf-8"))

        if "access_token" not in token_data:
Confidence
95% confidence
Finding
The code places the WeChat app secret directly into the URL query string when requesting an access token. Query parameters are commonly exposed in proxy logs, monitoring systems, exception messages, and upstream infrastructure, so this increases the chance of credential disclosure even though the destination host is legitimate.

Tainted flow: 'req' from os.environ.get (line 290, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
req = Request(url, data=body, headers=headers, method=method)

    try:
        with urlopen(req, timeout=30) as resp:
            resp_data = resp.read().decode("utf-8")
            return json.loads(resp_data) if resp_data else {}
    except HTTPError as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 290, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
req = Request(url, data=body, headers=headers, method=method)

    try:
        with urlopen(req, timeout=30) as resp:
            resp_data = resp.read().decode("utf-8")
            return json.loads(resp_data) if resp_data else {}
    except HTTPError as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 290, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
req = Request(url, data=body, headers=headers, method=method)

    try:
        with urlopen(req, timeout=30) as resp:
            resp_data = resp.read().decode("utf-8")
            return json.loads(resp_data) if resp_data else {}
    except HTTPError as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'req' from os.environ.get (line 290, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
f"grant_type=client_credential&appid={appid}&secret={secret}"
        )
        req = Request(token_url, method="GET")
        with urlopen(req, timeout=30) as resp:
            token_data = json.loads(resp.read().decode("utf-8"))

        if "access_token" not in token_data:
Confidence
90% confidence
Finding
The WeChat token request embeds the app secret directly in the URL query string. Secrets in URLs are commonly exposed via logs, proxies, monitoring systems, crash reports, and intermediary infrastructure, making credential leakage more likely even when using HTTPS.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
This mismatch is security-relevant because the manifest frames the skill as content creation/optimization, while the body also authorizes real external publishing, scheduling, file-system writes, and credential-backed network operations. Hidden or under-declared side effects increase the chance that a user or orchestrator invokes the skill without realizing it can perform irreversible actions on external accounts or local content stores.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
This mismatch is security-relevant because the manifest frames the skill as content creation/optimization, while the body also authorizes real external publishing, scheduling, file-system writes, and credential-backed network operations. Hidden or under-declared side effects increase the chance that a user or orchestrator invokes the skill without realizing it can perform irreversible actions on external accounts or local content stores.

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
This mismatch is security-relevant because the manifest frames the skill as content creation/optimization, while the body also authorizes real external publishing, scheduling, file-system writes, and credential-backed network operations. Hidden or under-declared side effects increase the chance that a user or orchestrator invokes the skill without realizing it can perform irreversible actions on external accounts or local content stores.

Credential Access

High
Category
Privilege Escalation
Content
| 变量 | 必需 | 说明 |
|------|------|------|
| `CE_TWITTER_BEARER_TOKEN` | 否 | Twitter API v2 Bearer Token |
| `CE_LINKEDIN_ACCESS_TOKEN` | 否 | LinkedIn API Access Token |
| `CE_WECHAT_APPID` | 否 | 微信公众号 AppID |
| `CE_WECHAT_SECRET` | 否 | 微信公众号 AppSecret |
| `CE_MEDIUM_TOKEN` | 否 | Medium Integration Token |
Confidence
89% confidence
Finding
The skill is explicitly designed to use high-value platform credentials from environment variables and then perform network actions on external accounts. Credential handling is expected in this context, but it still materially increases risk because any overbroad prompt trigger, missing permission scope, logging mistake, or unsafe script behavior could result in token misuse, unauthorized posting, or credential exposure.

Ae1

High
Category
analysis-evasion
Content
python3 scripts/obsidian_sync.py --action connect --data '{"vault_path":"~/MyVault"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/obsidian_sync.py --action connect --data '{"vault_path":"~/MyVault"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/obsidian_sync.py --action connect --data '{"vault_path":"~/MyVault"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/obsidian_sync.py --action connect --data '{"vault_path":"~/MyVault"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/obsidian_sync.py --action connect --data '{"vault_path":"~/MyVault"}'
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The import path is constructed from user-controlled file_rel via os.path.join(vault_path, file_rel) and then opened directly, with no canonicalization or containment check. This allows path traversal to read files outside the Obsidian vault, potentially exposing sensitive local files if an attacker can influence the import request.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The export path is built with os.path.join(vault_path, file_rel) and then written without validating that the resolved path stays inside the configured vault. An attacker or untrusted caller can supply traversal segments such as ../ to overwrite arbitrary files accessible to the process, which is especially dangerous because this function creates parent directories and writes attacker-controlled content.

Credential Access

High
Category
Privilege Escalation
Content
def _publish_linkedin(adapted: Dict[str, Any]) -> Dict[str, Any]:
    """发布到 LinkedIn。

    使用 LinkedIn API,通过 Access Token 认证。

    Args:
        adapted: 已适配的 LinkedIn 内容。
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill describes capabilities that read environment variables, access local files, write blog/markdown output, and publish over external networks, but it declares no explicit tool scope or permission boundary. In an agent environment, this creates overbroad authority and makes accidental or unauthorized file access, token-backed publishing, or network actions harder to constrain and audit.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
该行明确写明“你始终使用中文与用户沟通”,这构成了固定语言策略,并未说明用户可选择其他语言或进行语言偏好设置。根据规则,强制特定语言而无用户 opt-in 属于自然语言政策违规。

Vague Triggers

Medium
Confidence
91% confidence
Finding
Broad trigger phrases for content management can cause the skill to activate on ordinary conversation and begin creating, updating, deleting, importing, or exporting content unexpectedly. In a skill with file-write and publish pathways, overbroad activation meaningfully increases the risk of unintended state changes or data handling.

Vague Triggers

Medium
Confidence
92% confidence
Finding
Ambiguous publish/adapt triggers are dangerous because this skill can perform real network-backed publication and scheduling using stored credentials. A fuzzy match on phrases like publishing or distribution could lead to unintended previews, adaptations, or even posting workflows against external platforms.

Vague Triggers

Medium
Confidence
88% confidence
Finding
“查看数据”“内容表现”“指标报告”均是较常见表达,再加上“或类似意图”使触发范围进一步扩大。文档未说明这些短语必须关联到该技能支持的平台或内容对象,容易产生误触发。

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
该处以行为准则形式要求“始终使用中文与用户沟通”,再次固化了单一语言要求。文档没有提供切换语言的例外条件、用户选择入口或区域合规说明。

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This markdown file instructs users to set access tokens, app secrets, and vault paths, but it does not include any warning about protecting those credentials, avoiding shell history leakage, or limiting exposure of sensitive environment variables. Because the skill interacts with external publishing platforms and sensitive auth data, the description should explicitly warn users about credential sensitivity and safe storage.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
This markdown file presents all operational guidance in Chinese, including headings and best-practice instructions, but does not indicate that the user can select another language or that the skill is intentionally limited to a Chinese-speaking audience. Under the policy, forcing a specific language without user opt-in is a natural-language policy violation.

Static analysis

No suspicious patterns detected.