Back to skill

Security audit

Fin Ai

Security checks for vulnerabilities and agentic risk

Overview

This portfolio skill is mostly coherent, but it has real file-write and network-request safety gaps that should be reviewed before installation.

Install only if you trust the portfolio directories and JSON inputs you will process. Avoid running direct write scripts on untrusted holdings or snapshot files, review config.json before analysis, and do not allow arbitrary yahoo_base or proxy values. The skill should ideally validate dates as YYYY-MM-DD and restrict market-data endpoints before broad use.

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

Error
Location
scripts/holdings_sync.py:198
Finding
Unvalidated Date Values Allow Arbitrary JSON File Writes Outside the Portfolio Directory## Vulnerability Details **File Location**: `scripts/holdings_sync.py:198-201` **Related Location**: `scripts/persist_snapshot.py:56-61` **Vulnerability Type**: Path traversal and unrestricted file write **Risk Level**: High ### Vulnerable Code ```python def write_holdings(portfolio_dir: Path, holdings_payload: dict[str, Any]) -> Path: holdings_path = portfolio_dir / "holdings" / f"{holdings_payload['date']}.json" _write_json(holdings_path, holdings_payload) return holdings_path ``` The same pattern is present in snapshot persistence: ```python def write_snapshot(portfolio_dir: Path, snapshot: dict[str, Any]) -> Path: snap_dir = portfolio_dir / "snapshots" snap_dir.mkdir(parents=True, exist_ok=True) snap_path = snap_dir / f"{snapshot['date']}.json" snap_path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8") return snap_path ``` ### Technical Analysis The `date` field is used directly as a filename without enforcing the documented `YYYY-MM-DD` format or checking that the resulting path remains inside the intended `holdings` or `snapshots` directory. In `holdings_sync.py`, validation only verifies that the top-level `date` field exists. Normalization converts it to a stripped string but does not reject absolute paths, directory separators, `..` components, or non-date values. With `pathlib`, joining a directory with an absolute path discards the preceding directory. For example: ```python Path("/portfolio/holdings") / "/tmp/controlled.json" ``` resolves to `/tmp/controlled.json`. Because the implementation appends `.json`, an input date such as `/tmp/controlled` results in a write to `/tmp/controlled.json`. Relative traversal strings can similarly escape the portfolio directory. The generic `_write_json()` helper also creates parent directories, increasing the reachable write scope: ```python def _write_json(path: Path, payload ...[truncated 2388 chars]
Remediation
## Remediation Suggestions 1. Strictly validate all dates before using them in paths: ```python from datetime import datetime def validate_date(value: Any) -> str: if not isinstance(value, str): raise ValueError("date must be a YYYY-MM-DD string") try: parsed = datetime.strptime(value, "%Y-%m-%d") except ValueError as exc: raise ValueError("date must use YYYY-MM-DD format") from exc normalized = parsed.strftime("%Y-%m-%d") if normalized != value: raise ValueError("date is not canonical") return normalized ``` 2. Apply this validation in holdings synchronization, portfolio analysis, snapshot persistence, and the refresh CLI. 3. Resolve and confine every generated path before writing: ```python base = (portfolio_dir / "holdings").resolve() destination = (base / f"{validated_date}.json").resolve() if destination.parent != base: raise HoldingsSyncError("Holdings path escapes the holdings directory") ``` 4. Reject absolute paths, `/`, `\`, null bytes, and traversal components independently of date parsing as defense in depth. 5. Refuse to write through symbolic links, or use secure file-opening primitives with no-follow behavior where supported. 6. Write to a temporary file in the same trusted directory, flush and synchronize it, and atomically replace the destination. 7. Add regression tests for absolute paths, `../` traversal, Windows separators, malformed dates, symlink destinations, and direct invocation of every persistence entry point.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/market_context.py:219
Finding
Attacker-Controlled Market Configuration Enables Server-Side Request Forgery## Vulnerability Details **File Location**: `scripts/market_context.py:219-240` **Vulnerability Type**: Server-side request forgery through configurable HTTP endpoint **Risk Level**: Medium ### Vulnerable Code ```python proxy = config.get("proxy", "") proxies = {"http": proxy, "https": proxy} if proxy else None headers = {"User-Agent": "Mozilla/5.0"} ticker_map = config.get("ticker_map", {}) yahoo_base = config.get("yahoo_base", "https://query1.finance.yahoo.com/v8/finance/chart") fx_tickers = config.get("fx_tickers", {"HKD_CNY": "HKDCNY=X", "USD_CNY": "USDCNY=X"}) prices: dict[str, float] = {} currencies: dict[str, str] = {} fx_rates: dict[str, float] = {"CNY": 1.0} warnings: list[str] = [] meta: dict[str, Any] = {} all_tickers = { pos["ticker"] for group in holdings.get("groups", {}).values() for pos in group.get("positions", []) } for ticker in all_tickers: if ticker in prices: continue yahoo_symbol = ticker_to_yahoo_symbol(ticker, ticker_map) try: exchange, code = ticker.split(":") if exchange == "OTC": nav, currency, source = _fetch_otc_nav(code, headers=headers, proxies=proxies) prices[ticker] = nav currencies[ticker] = currency meta[ticker] = {"source": source, "fallback": source != "fundgz_estimate"} if source == "pingzhongdata": add_warning(warnings, f"{ticker} 当日估值不可用,已回退为最近披露净值") continue data = _request_json_with_retry( f"{yahoo_base}/{yahoo_symbol}", headers=headers, proxies=proxies, timeout=15, params={"interval": "1d", "range": "1d"}, ) ``` The request helper performs the supplied request without validating the destination: ```python response = requests.get( url, headers=headers, proxies=proxies, timeout=timeout, ...[truncated 2942 chars]
Remediation
## Remediation Suggestions 1. Remove arbitrary `yahoo_base` customization unless it is strictly required. 2. Maintain an explicit allowlist of approved HTTPS quote-service hostnames. 3. Require the `https` scheme and reject URLs containing user information, fragments, unexpected ports, or nonstandard path components. 4. Resolve destination hostnames and reject loopback, private, link-local, multicast, reserved, and unspecified IPv4 and IPv6 addresses. 5. Revalidate the destination after every DNS resolution and redirect. Prefer disabling redirects: ```python response = requests.get( url, headers=headers, proxies=approved_proxies, timeout=timeout, params=params, allow_redirects=False, ) ``` 6. Do not accept arbitrary proxy URLs from an untrusted portfolio directory. Configure proxies through a trusted administrative channel and enforce an allowlist. 7. Validate ticker symbols against strict exchange-specific patterns before including them in URL paths. 8. Apply outbound firewall or sandbox rules so the process cannot reach loopback, private subnets, link-local metadata addresses, or unrelated external hosts. 9. Separate user-controlled portfolio data from trusted application configuration; do not implicitly trust `config.json` merely because it is located in a selected portfolio directory. 10. Add tests covering direct private IPs, IPv6 loopback, DNS rebinding scenarios, redirects to internal addresses, malformed schemes, embedded credentials, and attacker-controlled proxies.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill text explicitly authorizes optional external CLI use and market data injection, which implies network-sourced data and shell-style pipelines not reflected in a tightly scoped purpose statement. In a portfolio-management context, undeclared external data retrieval and local file reads can affect financial outputs, leak usage patterns, or expand the attack surface through untrusted market_context files and third-party data sources.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill text explicitly authorizes optional external CLI use and market data injection, which implies network-sourced data and shell-style pipelines not reflected in a tightly scoped purpose statement. In a portfolio-management context, undeclared external data retrieval and local file reads can affect financial outputs, leak usage patterns, or expand the attack surface through untrusted market_context files and third-party data sources.

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
The skill text explicitly authorizes optional external CLI use and market data injection, which implies network-sourced data and shell-style pipelines not reflected in a tightly scoped purpose statement. In a portfolio-management context, undeclared external data retrieval and local file reads can affect financial outputs, leak usage patterns, or expand the attack surface through untrusted market_context files and third-party data sources.

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill advertises and instructs use of filesystem access, configuration persistence, and optional external CLI/network-backed market data retrieval, but it declares no explicit tool scope or permission boundaries. In practice this increases the chance that an agent invokes broader file/network capabilities than the user expects, especially because the workflow includes writes to persistent directories and use of externally sourced pricing data.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
L003 明确将该技能描述为“中文投资组合管理 skill”,属于自然语言层面的语言/locale 限制。文件中未说明这是用户自选偏好,也未提供语言切换或 opt-in,因此可能违反语言选择政策。

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
The section header and bullet guidance at L65-L69 switch to Chinese while the rest of the document is in English. This creates a language-policy issue because the file imposes a specific language for part of the instructions without user opt-in or an explanation that the skill is intentionally region-specific.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
Line L22 switches from English to Chinese for a normative workflow rule, which can force a specific language on users or maintainers who are otherwise reading an English document. The file does not offer a language choice or explain that this contract is intended for a Chinese-language or region-specific audience.

Natural-Language Policy Violations

Medium
Confidence
98% confidence
Finding
This code includes multiple user-facing error messages and command-line help strings in Chinese only, such as validation errors and argparse descriptions. Because the file does not provide a language selection mechanism or justify a Chinese-only locale restriction, it violates the policy against forcing a specific language without user opt-in.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The function does more than initialize a portfolio directory: by default it also persists global user settings that change the default profile and portfolio path. In a skill focused on holdings synchronization and snapshot generation, silently changing defaults can redirect later operations to an attacker-chosen or unintended data directory, causing integrity issues and surprising cross-workflow side effects.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The module docstring claims that persistent writes occur only after explicit confirmation, but main() calls init_portfolio on the resolved real portfolio directory before confirm_write is checked. If the target directory does not exist, this creates or modifies real on-disk state during what the user would reasonably expect to be a non-persistent preview run, violating the safety guarantee and potentially causing unintended filesystem changes.

Intent-Code Divergence

Low
Confidence
91% confidence
Finding
The docstring frames the script as only creating a user portfolio data directory. In practice, when set_default is enabled, the script also writes persistent user settings that change the default profile and default portfolio directory, which is a materially different side effect.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
This Python file contains user-visible status/error text and command-line descriptions entirely in Chinese, including the settings warning and argparse help strings. The policy flags language constraints when a specific language is forced without user opt-in, and there is no indication here that users can choose locale or that the tool is intentionally region-specific.

Natural-Language Policy Violations

Low
Confidence
97% confidence
Finding
This Python file contains multiple natural-language warning messages presented in Chinese, such as fallback/error notices added to the returned warnings list. There is no indication in the file that the user has opted into Chinese or that the skill is explicitly limited to a Chinese-language or region-specific audience, so this creates a language/locale policy concern.

Missing User Warnings

Low
Confidence
81% confidence
Finding
The function writes a JSON settings file under the user's home directory, which is a file write affecting persistent user data. In this code, there is no confirmation prompt, user-facing log/print, or comment/docstring disclosing that the operation persists settings to disk.

Missing User Warnings

Low
Confidence
88% confidence
Finding
This function creates multiple directories and may write a new config.json file, which are persistent filesystem modifications. The code does not include a confirmation prompt, user-visible logging, or explanatory docstring/comment indicating that it will initialize files on disk.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
This Python file contains user-visible error strings in Chinese while other CLI/help text in the same skill is in English. That creates an implicit language constraint without user opt-in, which matches the locale-policy concern for natural-language content.

Natural-Language Policy Violations

Low
Confidence
87% confidence
Finding
The validation error message on this line is presented only in Chinese, while other user-facing descriptions in the file are English. This inconsistency can force a language/locale on users without explicit choice or justification.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The command-line interface mostly uses English, but this help string is in Chinese and there is no indication that the tool is intentionally region-specific. That is a natural-language locale inconsistency and may violate language-choice policy.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
This code includes natural-language warning messages in Chinese that are surfaced in the output, and the CLI help text also uses Chinese. Because the skill does not offer a language or locale choice, it effectively forces a specific language on users, which matches the locale-policy violation criteria.

Static analysis

No suspicious patterns detected.