Back to skill

Security audit

yinxiang-notes

Security checks for vulnerabilities and agentic risk

Overview

This Evernote China skill mostly matches its stated purpose, but it needs Review because it handles account tokens unsafely, includes an undocumented note export script, and can permanently delete or persist note data.

Install only if you are comfortable giving the skill broad access to your Yinxiang/Evernote account and writing note contents to local disk. Remove token-printing debug output, validate the NoteStore URL, delete or document get_note_enml.py, use a dedicated skill-scoped secret location, and require explicit confirmation before permanent trash deletion.

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

T09 · Insecure Skill Coding Practices

Note
Location
scripts/list_tags.py:34
Finding
Developer Token Prefix Disclosed Through Standard Output<![CDATA[ ## Vulnerability Details **File Location**: `scripts/list_tags.py:34` **Vulnerability Type**: Sensitive credential disclosure through logs **Risk Level**: Low ### Vulnerable Code ```python print(f"Token: {token[:30] if token else 'None'}...") ``` ### Technical Analysis The script prints the first 30 characters of the Evernote Developer Token to standard output. Developer Tokens contain structured authentication information, and even a partial value is sensitive. Standard output may be retained in terminal history, CI logs, agent transcripts, monitoring systems, or shared debugging records. Although this does not reveal the complete token, it unnecessarily exposes a substantial credential prefix and enables token identification, correlation across logs, and disclosure of structured token metadata. ### Attack Path 1. A user, automation system, or agent runs `scripts/list_tags.py`. 2. The script reads `EVERNOTE_TOKEN` from the workspace `.env` file. 3. The first 30 characters are printed to standard output. 4. An attacker with access to terminal output, CI logs, or agent transcripts obtains the token prefix. 5. The disclosed fragment can be used for credential correlation or combined with information exposed elsewhere. ### Impact Assessment The issue does not independently provide authenticated access because the entire token is not printed. However, it exposes sensitive authentication material to every system that captures process output. The scope includes the Evernote account associated with the token if the partial disclosure can be combined with another leak. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Remove all output containing any portion of the token. - Replace the statement with a boolean status message, for example: ```python print(f"Token loaded: {'yes' if token else 'no'}") ``` - Ensure exceptions and debug logs never include request headers or configuration values. - Add automated secret-redaction tests covering console and error output. - Treat previously collected logs as sensitive and remove exposed token fragments where practical. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/list_tags.py:25
Finding
Developer Token Can Be Transmitted to an Unrestricted Configured Endpoint<![CDATA[ ## Vulnerability Details **File Location**: `scripts/list_tags.py:25-46` **Vulnerability Type**: Missing validation of a credential-bearing network destination **Risk Level**: Medium ### Vulnerable Code ```python if os.path.exists(env_path): with open(env_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if line.startswith('EVERNOTE_TOKEN='): token = line.split('=', 1)[1].strip() elif line.startswith('EVERNOTE_NOTESTORE_URL='): note_store_url = line.split('=', 1)[1].strip() print(f"Token: {token[:30] if token else 'None'}...") print(f"URL: {note_store_url}") import evernote.edam.notestore.NoteStore as NoteStore import thrift.transport.THttpClient as THttpClient import thrift.protocol.TBinaryProtocol as TBinaryProtocol def list_tags(): print("\n🔄 正在获取标签...") transport = THttpClient.THttpClient(note_store_url) transport.setCustomHeaders({"Authorization": f"Bearer {token}"}) ``` Equivalent configuration and transport behavior is used throughout the API scripts. ### Technical Analysis `EVERNOTE_NOTESTORE_URL` is accepted directly from `.env` without validating its scheme or hostname. The Developer Token is then placed in the `Authorization` header for requests to that destination. Consequently, a modified or incorrectly configured `.env` file can direct the request and its bearer credential to an arbitrary server. The implementation does not enforce HTTPS or restrict the destination to documented Evernote China hosts. This finding requires an attacker to modify configuration or induce a user to use a malicious endpoint. It is not evidence of a built-in attacker-controlled endpoint. ### Attack Path 1. An attacker gains the ability to modify the workspace `.env` file or convinces the user to use a supplied configuration. 2. The attacker sets `EVERNOTE_NOTESTORE_URL` to a server under the attacker's control. 3. The user or agent ...[truncated 722 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse the URL and require the `https` scheme. - Allowlist only the documented service hosts, such as: - `app.yinxiang.com` - `sandbox.yinxiang.com` - Reject embedded credentials, unexpected ports, malformed URLs, and non-allowlisted hosts. - Verify redirect behavior and prevent credentials from being forwarded to another hostname. - Prefer deriving the NoteStore endpoint from a trusted service response or fixed environment selection rather than accepting an arbitrary URL. - Fail closed when the endpoint is absent or invalid. - Apply the same validation centrally to every API script rather than duplicating configuration logic. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/get_note_enml.py:12
Finding
Undocumented Hard-Coded Note Exporter Writes Raw Content to Plaintext<![CDATA[ ## Vulnerability Details **File Location**: `scripts/get_note_enml.py:12-38` **Vulnerability Type**: Undocumented sensitive-data export and plaintext persistence **Risk Level**: Medium ### Vulnerable Code ```python TARGET_GUID = "97701f2b-0a68-468c-bb09-7fe646b521ce" OUTPUT_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "note_enml_output.xml") def main(): token, note_store_url = load_config() if not token: print("❌ 错误: 未找到 EVERNOTE_TOKEN") return transport = THttpClient.THttpClient(note_store_url) transport.setCustomHeaders({"Authorization": f"Bearer {token}"}) protocol = TBinaryProtocol.TBinaryProtocol(transport) note_store = NoteStore.Client(protocol) print(f"📥 获取笔记: {TARGET_GUID}") try: note = note_store.getNote(token, TARGET_GUID, True, True, False, False) except Exception as e: print(f"❌ 获取笔记失败: {e}") return print(f"标题: {note.title}") print(f"内容长度: {len(note.content)} 字符") with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: f.write(note.content) ``` ### Technical Analysis The script contains a fixed note GUID, retrieves that note using the configured Developer Token, and writes its raw ENML content to `scripts/note_enml_output.xml`. The script is not included in the script inventories in `README.md` or `SKILL.md`. The fixed identifier makes the behavior account- or note-specific rather than accepting an explicit user-selected note. The output is written as ordinary plaintext without requesting an output location, setting restrictive file permissions, warning about sensitive content, or cleaning up the exported file. There is no evidence that the file is transmitted to an external attacker. The confirmed risk is unexpected local extraction and persistence. ### Attack Path 1. A Developer Token with access to the hard-coded note is configured. 2. A user, automation system, or agent invokes `scripts/get_note_enml.py`, poten ...[truncated 643 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the script if it is a development or debugging artifact. - Otherwise, document it in both `README.md` and `SKILL.md`. - Replace the hard-coded GUID with a required `--guid` argument. - Require an explicit `--output` destination and display a sensitive-data warning before writing. - Refuse to overwrite existing files unless a separate confirmation flag is supplied. - Create output files with owner-only permissions where supported. - Avoid writing exports inside the source tree by default. - Add generated export filenames to ignore rules and provide a secure cleanup option. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/empty_trash.py:69
Finding
Permanent Trash Deletion Can Bypass the CLI Confirmation Prompt<![CDATA[ ## Vulnerability Details **File Location**: `scripts/empty_trash.py:69-116` **Vulnerability Type**: Destructive API operation without an internal confirmation guard **Risk Level**: High ### Vulnerable Code ```python def empty_trash(): """清空废纸篓""" token, note_store_url = load_config() if not token: print("❌ 错误: 未找到 EVERNOTE_TOKEN") return False if not note_store_url: note_store_url = "https://app.yinxiang.com/shard/s16/notestore" print(f"🔄 正在连接印象笔记...") transport = THttpClient.THttpClient(note_store_url) transport.setCustomHeaders({"Authorization": f"Bearer {token}"}) protocol = TBinaryProtocol.TBinaryProtocol(transport) note_store = NoteStore.Client(protocol) print("✅ 连接成功") print() print("🔍 扫描废纸篓中的笔记...") deleted_notes = find_deleted_notes(note_store, token) print(f"📋 废纸篓中共有 {len(deleted_notes)} 条笔记") print() if len(deleted_notes) == 0: print("✅ 废纸篓已是空的") return True for note in deleted_notes: print(f" 🗑️ {note.title}") print() print("⚠️ 即将永久删除所有废纸篓中的笔记...") print() count = 0 for note in deleted_notes: try: note_store.expungeNote(token, note.guid) count += 1 print(f" ✅ 永久删除: {note.title}") except Exception as e: print(f" ❌ 删除失败: {note.title} - {e}") print() print(f"✅ 清空完成!共永久删除 {count} 条笔记") return True ``` The command-line entry point prompts for Enter, but that prompt exists outside `empty_trash()` and therefore does not protect direct function calls. ### Technical Analysis `empty_trash()` performs irreversible `expungeNote` operations without requiring a confirmation argument. The interactive prompt in the `__main__` block only protects ordinary command-line execution. Any caller that imports the module and directly invokes `empty_trash()` bypasses the prompt completely. The function enumerates up to 500 notes t ...[truncated 1030 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Change the function signature so destructive behavior is denied by default: ```python def empty_trash(confirm=False, dry_run=True): if not confirm: print("Permanent deletion was not confirmed.") return False ``` - Require an explicit confirmation value inside the function before any `expungeNote` call. - Add a `--dry-run` mode that is the default and only lists affected notes. - Require a deliberate CLI flag such as `--confirm-permanent-deletion`, not merely pressing Enter. - Consider requiring the account identifier or number of notes as an additional confirmation value. - Log only non-sensitive deletion metadata and return a failure status when confirmation is absent. - Add tests proving that direct imports and default calls cannot permanently delete notes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/sync_to_obsidian.py:509
Finding
Unsanitized Note HTML Is Persisted as Renderable Web-Clip Content<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sync_to_obsidian.py:509-514` **Vulnerability Type**: Unsafe storage and rendering of active HTML content **Risk Level**: Medium ### Vulnerable Code ```python clip_filename = f"clip_{meta.guid[:8]}.html" clip_dir = os.path.join(nb_folder, '_clips') clip_fp = os.path.join(clip_dir, clip_filename) with open(clip_fp, 'w', encoding='utf-8') as f: f.write(f"<!-- source_guid: {meta.guid} -->\n") f.write(f"<!-- notebook: {nb.name} -->\n") f.write(make_clip_html(original_content or '', hash_to_file)) ``` The HTML conversion function only replaces ENML media and removes wrapper declarations: ```python c = enml_content for _ in range(100): new_c = replace_en_media_html(c) if new_c == c: break c = new_c c = re.sub(r'<\?xml[^?]*\?>', '', c) c = re.sub(r'<!DOCTYPE[^>]*>', '', c) c = re.sub(r'<en-note[^>]*>', '<div>', c) c = re.sub(r'</en-note>', '</div>', c) return c.strip() ``` ### Technical Analysis Long clipped notes identified as ENML web clips are written to `.html` files. `make_clip_html()` does not sanitize active elements or attributes. It does not remove scripts, inline event handlers, iframes, unsafe URL schemes, forms, or remote resource references. If hostile content reaches an Evernote note—such as through a malicious web clip, shared note, compromised account, or imported content—the synchronized HTML preserves that active content. Execution depends on how the resulting file is opened. The risk becomes exploitable when it is rendered by a browser or an Obsidian plugin that permits active HTML behavior. ### Attack Path 1. An attacker causes a malicious or compromised web clip to exist in the Evernote account. 2. The note contains active HTML, such as script-capable elements, event handlers, unsafe links, or remote resources. 3. The user runs `sync_to_obsidian.py`. 4. The script classifies the content as a sufficiently large clipped note. 5. `make_clip_html()` ...[truncated 729 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Sanitize generated HTML with a maintained strict-allowlist sanitizer. - Remove at minimum: - `<script>`, `<iframe>`, `<object>`, `<embed>`, and active form elements - all inline event attributes such as `onclick` and `onload` - `javascript:`, unsafe `data:`, and other executable URL schemes - automatic remote-resource references unless explicitly permitted - Consider converting all clips to inert Markdown or escaped source text. - If original HTML must be retained, store it with a non-renderable extension and require an explicit export step. - Add a prominent warning that generated clips are untrusted content. - Test the sanitizer against malformed markup and common HTML injection payloads. - Recommend that viewers use sandboxing and disable script execution and remote resource loading. ]]>
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
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (40)

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')

    token = None
    note_store_url = None
Confidence
72% confidence
Finding
The script walks up to a workspace-level .env and reads Evernote credentials from there, which broadens the trust boundary beyond the script's own directory. In an agent/skill ecosystem, this can unintentionally consume secrets from a shared workspace and make cross-skill secret exposure more likely if an untrusted skill is introduced or logs/errors reveal the loaded values.

Missing User Warnings

High
Confidence
97% confidence
Finding
This code loads a live Evernote token from a workspace .env file and then exposes part of it in console output while establishing the connection. In an agent skill context, console output may be captured by orchestrators, shared logs, or other tools, making credential leakage more dangerous than in a purely local one-off script.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_config():
    """从 .env 文件加载配置"""
    script_dir = os.path.dirname(__file__)
    skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')
    
    token = None
    note_store_url = None
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')
    
    token = None
    note_store_url = None
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')
    
    token = None
    note_store_url = None
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')
    
    token = None
    note_store_url = None
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')
    
    token = None
    note_store_url = None
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(script_dir)
    skills_dir = os.path.dirname(skill_dir)
    workspace_dir = os.path.dirname(skills_dir)
    env_path = os.path.join(workspace_dir, '.env')
    
    token = None
    note_store_url = None
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
skill_dir = os.path.dirname(scripts_dir)
skills_dir = os.path.dirname(skill_dir)
workspace_dir = os.path.dirname(skills_dir)
env_path = os.path.join(workspace_dir, '.env')

print(f"工作目录: {workspace_dir}")
print(f"ENV文件: {env_path}")
Confidence
93% confidence
Finding
The script explicitly locates and reads a workspace-level .env file to obtain an Evernote bearer token, which is credential access behavior beyond the narrow need of a simple tag-listing utility and broadens the blast radius to any shared workspace secrets. In the context of an agent skill, this is more dangerous because skills may run in semi-automated environments where stdout/logging and cross-skill file access can expose credentials without the user noticing.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This markdown file lists `empty_trash.py` as permanently deleting the trash, but provides no caution, confirmation note, or warning about data loss. Because the operation is destructive and irreversible, the skill description should explicitly warn users before they run it.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill documents and encourages local file read/write behavior by synchronizing notes into a local Obsidian vault, but it does not declare any explicit tool scope such as allowed-tools or permissions. That creates a capability-transparency problem: an agent or reviewer may not realize the skill can modify local files, increasing the chance of unintended data writes, overwrites, or exposure of synced note content on the host filesystem.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The skill documentation is entirely in Chinese and explicitly targets users of the China-specific Yinxiang service, but it does not indicate any language choice or opt-in for users who may expect another locale. Under the policy rule, forcing a specific language without user opt-in is a natural-language policy concern.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The file's natural-language usage text and runtime messages are written only in Chinese, which imposes a specific language choice on users without offering an alternative or opt-in. Under the policy, forcing a language or locale without user choice is a natural-language policy violation unless clearly justified as region-specific.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The script prints the first 25 characters of the Evernote developer token to stdout during normal operation. Even partial credential disclosure is unnecessary for note creation and can leak sensitive material into terminal history, CI logs, chat transcripts, or monitoring systems, reducing the effort needed for token theft or correlation.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The module docstring presents the script description and usage solely in Chinese, and the rest of the script continues with Chinese-only user-facing messages. This enforces a specific language for interaction without any opt-in or alternative, which matches the language/locale policy violation criteria.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The docstring and all user-facing prompts are written only in Chinese, which constitutes a language/locale constraint in the skill's natural-language interface. The file does not offer an opt-in language choice or explain that the skill is intentionally limited to a Chinese-speaking or region-specific audience.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The script retrieves the full content of a specified Evernote/Yinxiang note and writes it directly to a local file on disk without any confirmation, warning, access control, or cleanup. Because notes can contain sensitive personal or business data, silent persistence increases the risk of unintended local disclosure through shared machines, backups, or other processes reading the file.

Static analysis

No suspicious patterns detected.