Back to skill

Security audit

trading-sop

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent as a trading-analysis orchestrator, but its optional local dashboard exposes report data through unsafe unauthenticated web APIs and vulnerable HTML rendering.

Install only if you are comfortable with this skill producing actionable trading analysis and writing reports into your workspace. Do not run the dashboard on a non-localhost interface, avoid opening untrusted/imported report files in it, and treat its recommendations as informational rather than financial advice.

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
web/index.html:1607
Finding
Stored DOM Cross-Site Scripting Through Unsanitized Report Content and Metadata<![CDATA[ ## Vulnerability Details **File Location**: `web/index.html:1488-1501`, `web/index.html:1557-1609`, `web/index.html:1720-1800`, and `scripts/report_indexer.py:246-260` **Vulnerability Type**: Stored DOM cross-site scripting **Risk Level**: High ### Vulnerable Code The report body is transformed by a custom Markdown parser and assigned directly to `innerHTML`: ```javascript // web/index.html:1607-1609 const mdSurface = document.getElementById('reportMarkdownBody'); mdSurface.innerHTML = parseAdvancedMarkdown(data.body_markdown || data.raw_content); ``` The custom parser does not escape ordinary HTML or sanitize the generated output: ```javascript // web/index.html:1720-1800 function parseAdvancedMarkdown(md) { if (!md) return ''; let text = md; // 1. Code blocks with copy button text = text.replace(/```([a-zA-Z0-9_-]*)\n([\s\S]*?)```/gm, (match, lang, code) => { const escaped = escapeHtml(code); return ` <div class="code-block-wrapper"> <div class="code-block-header"> <span>${lang || 'text'}</span> <button class="code-copy-btn" aria-label="复制代码块" onclick="copySnippet(this)" data-code="${encodeURIComponent(code)}">复制</button> </div> <pre><code class="lang-${lang}">${escaped}</code></pre> </div> `; }); // 2. Callout Cards text = text.replace(/^\>\s+\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]\s*\n((?:\>.*(?:\n|$))*)/gim, (m, type, body) => { const cleanBody = body.replace(/^\>\s?/gm, '').trim(); const typeLower = type.toLowerCase(); let icon = 'ℹ️'; if (typeLower === 'tip') icon = '💡'; else if (typeLower === 'warning') icon = '⚠️'; else if (typeLower === 'important') icon = '⚡'; else if (typeLower === 'caution') icon = '🛑'; return ` <div class="callout-card ${typeLower}"> <div class="callout-header"> <span>${icon}</span> <span>${type}</span> </div> <div>${parseInlineMarkdown(cleanBody)} ...[truncated 5069 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace the custom parser with a maintained Markdown library configured to reject or escape raw HTML. 2. Sanitize all rendered HTML using a strict allowlist sanitizer such as DOMPurify before assigning it to the DOM. 3. Prefer a configuration that disables raw HTML completely unless it is explicitly required. 4. Render metadata using `textContent`, `createElement()`, and `setAttribute()` rather than interpolating values into HTML strings. 5. Remove inline `onclick` handlers and register handlers with `addEventListener()`. 6. Pass report paths through JavaScript closures or `data-*` properties rather than embedding them in executable JavaScript. 7. Validate frontmatter fields such as `code`, `category`, `rating`, and dates against explicit formats. 8. Add a restrictive Content Security Policy that disallows inline scripts and event handlers. 9. Add regression tests covering: - `<img src=x onerror=...>` - `<svg onload=...>` - Malicious frontmatter values - Apostrophes and quotes in report filenames - Nested HTML in tables, headings, and callouts ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/server.py:26
Finding
Unauthenticated Cross-Origin Access to Local Report APIs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/server.py:26-32`, `scripts/server.py:39-65`, `scripts/server.py:101-105`, and `scripts/server.py:137-153` **Vulnerability Type**: Missing authentication and overly permissive CORS **Risk Level**: Medium ### Vulnerable Code Every JSON response permits access from any web origin: ```python # scripts/server.py:26-32 def send_json(self, data: object, status: int = 200) -> None: body = json.dumps(data, ensure_ascii=False).encode("utf-8") self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Content-Length", str(len(body))) self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() self.wfile.write(body) ``` Report listing and report-detail routes require no authentication: ```python # scripts/server.py:39-65 if path == "/api/reports": category = query.get("category", [None])[0] reports = scan_reports(self.report_dir) if category and category != "all": reports = [r for r in reports if r.get("category") == category] self.send_json(reports) return if path == "/api/report": rel_path = query.get("path", [None])[0] if not rel_path: self.send_error_json("Missing 'path' parameter", 400) return detail = get_report_detail(self.report_dir, rel_path) if not detail: self.send_error_json(f"Report not found: {rel_path}", 404) return self.send_json(detail) return ``` The SVG route is similarly cross-origin accessible: ```python # scripts/server.py:101-105 self.send_response(200) self.send_header("Content-Type", "image/svg+xml; charset=utf-8") self.send_header("Content-Length", str(len(svg_content))) self.send_header("Access-Control-Allow-Origin", "*") self.end_headers() ``` Although the default binding is loopback, the command-line interface allows exposure on any interface without adding authentication: ```python # scripts/serve ...[truncated 3019 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `Access-Control-Allow-Origin: *`. 2. Permit only the dashboard’s exact expected origin, or omit CORS entirely when the UI and API share an origin. 3. Generate a cryptographically random access token at startup and require it for API requests. 4. Use an `HttpOnly`, `SameSite=Strict` session cookie or an equivalent capability-bound authentication mechanism. 5. Reject non-loopback host values by default. 6. Require an explicit security acknowledgment flag before binding to a non-loopback interface. 7. Print a prominent warning when the service is network-accessible. 8. Consider validating the `Host` and `Origin` headers to reduce DNS-rebinding and cross-origin risks. 9. Add the following defensive headers: - `Content-Security-Policy` - `X-Content-Type-Options: nosniff` - `Cache-Control: no-store` - `Referrer-Policy: no-referrer` - `X-Frame-Options: DENY` or CSP `frame-ancestors 'none'` 10. Add tests proving that unauthorized and unexpected-origin requests are rejected. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/report_indexer.py:172
Finding
Report Indexing Can Read Metadata From Symlinked Markdown Files Outside the Report Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/report_indexer.py:172-218` **Vulnerability Type**: Symlink-based directory-boundary bypass during report indexing **Risk Level**: Low ### Vulnerable Code The scanner recursively finds Markdown paths and reads them before verifying that their resolved targets remain inside the configured report directory: ```python # scripts/report_indexer.py:172-189 def scan_reports(report_dir: Path | str) -> list[dict[str, Any]]: """Recursively scan report directory and return structured metadata list.""" base_path = Path(report_dir).resolve() if not base_path.exists(): return [] reports: list[dict[str, Any]] = [] for file_path in sorted(base_path.rglob("*.md")): if file_path.name.startswith("."): continue try: content = file_path.read_text(encoding="utf-8") except OSError: continue rel_path = file_path.relative_to(base_path) frontmatter, body = parse_frontmatter(content) parent_dir = rel_path.parent.name if rel_path.parent != Path(".") else "" ``` The scanner subsequently exposes metadata and a content-derived summary: ```python # scripts/report_indexer.py:202-218 summary = merged.get("summary") if not summary: paragraphs = [p.strip() for p in body.split("\n\n") if p.strip() and not p.strip().startswith("#") and not p.strip().startswith(">")] summary = paragraphs[0][:150] + ("..." if len(paragraphs[0]) > 150 else "") if paragraphs else "" item = { "id": file_hash, "relative_path": str(rel_path), "filename": file_path.name, "title": merged.get("title", file_path.stem), "date": normalized_date, "category": cat, "category_label": CATEGORY_LABELS.get(cat, "通用"), "code": str(merged.get("code", "")), "ticker_name": str(merged.get("ticker_name", "")), "rating": str(merged.get("rating", "")), "veto_status": str(merged.get("veto_status", "")), "sum ...[truncated 2317 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject symbolic links before reading report candidates: ```python if file_path.is_symlink(): continue ``` 2. Resolve every candidate and verify containment before calling `read_text()`: ```python resolved_file = file_path.resolve() try: resolved_file.relative_to(base_path) except ValueError: continue if not resolved_file.is_file(): continue content = resolved_file.read_text(encoding="utf-8") ``` 3. Use the validated resolved path consistently for file operations. 4. Consider opening files with platform-specific no-follow semantics where available if the report directory can be modified concurrently. 5. Catch `UnicodeDecodeError` in addition to `OSError` to prevent malformed external targets from disrupting indexing. 6. Add tests for: - Symlinked Markdown files pointing outside the report directory. - Symlinks pointing to files inside the report directory. - Broken symlinks. - Symlink replacement races where relevant to the deployment model. ]]>
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • 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 (8)

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill declares no permissions, yet its documentation clearly instructs use of environment variables, reading and writing files under the workspace, and starting an HTTP service. This creates a transparency and consent gap: operators may invoke the skill expecting a pure orchestrator, while it can access local resources and expose data over a local network listener.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The documented purpose presents the skill as a trading-analysis orchestrator, but later content adds materially different behavior: local HTTP serving, report indexing, disk reads, database access, and chart generation. This mismatch is dangerous because reviewers and users may approve or invoke the skill under a narrower trust model than the behavior actually requires, increasing the risk of unintended data exposure from local files and services.

Description-Behavior Mismatch

Medium
Confidence
88% confidence
Finding
The skill is positioned primarily as analysis and advice, but SOP-C includes an execution segment that can trigger simulated trade placement when conditions are met. Even if labeled simulated, execution-oriented behavior changes the risk profile because users may not expect action-taking flows from an analysis orchestrator, especially in financial contexts where mistaken automation can influence decisions or downstream systems.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
This file implements a network-accessible HTTP dashboard and report server, which is materially outside the stated skill purpose of orchestrating A-share trading SOP analysis from natural-language input. Expanding a skill to expose web endpoints increases attack surface, can expose reports or market-analysis data to other local/network clients, and creates a capability gap between the manifest and actual behavior that undermines trust and review assumptions.

Vague Triggers

Medium
Confidence
85% confidence
Finding
The trigger list includes broad everyday phrases such as '研究一下', '操作建议', and 'SOP', which can cause accidental invocation outside the user's intended context. Because this skill can write files and potentially start workflows across multiple subskills, overbroad activation increases the chance of unintended data processing and side effects.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill instructs automatic creation and writing of reports into the current workspace without a clear upfront warning or consent step. Silent filesystem side effects are risky because they may overwrite expectations about a clean workspace, persist sensitive financial analysis locally, or place files in repositories that are later synced or committed.

Vague Triggers

Medium
Confidence
84% confidence
Finding
The eval prompts use very broad, everyday trading requests that can trigger the skill without clear scope boundaries or safety qualifiers. In an agentic trading workflow, this increases the chance of over-triggering investment-advice behavior, causing the system to produce actionable financial recommendations in ambiguous contexts or without required disclaimers, data checks, or user confirmation.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The SOP explicitly aims to produce buy/sell ratings, target prices, stop-loss lines, and position sizing, which are highly actionable financial recommendations. Without an explicit user-facing warning about uncertainty, suitability, and non-advisory status, users may over-rely on the output for real-money trading decisions, increasing risk of financial harm.

Static analysis

No suspicious patterns detected.