Back to skill

Security audit

Lse Trading Agent

Security checks for vulnerabilities and agentic risk

Overview

The skill is a disclosed LSE trading analysis and paper-portfolio tracker, but its risk checks and local portfolio writes are weak enough that users should review it before installing.

Install only if you are comfortable treating this as an educational paper-trading tool, not a dependable trading or risk-control system. Back up any portfolio JSON before using --init, --add, or --remove, prefer a trusted/pinned uv installation, and do not rely on its risk validation without independently checking position size, sector exposure, drawdown, and daily-loss limits.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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
Findings (6)

T01 · Skill Instruction Hijacking

Error
Location
scripts/sentiment.py:20
Finding

Untrusted Yahoo Finance headlines can influence agent instructions

Content
View full analysis
dict: """Fetch recent news headlines from yfinance for a given ticker.""" try: stock = yf.Ticker(ticker) raw_news = stock.news except Exception as e: return { "ticker": ticker, "headline_count": 0, "headlines": [], "error": f"Failed to fetch news: {e}", } headlines = [] for item in raw_news[:max_headlines]: try: content = item.get("content", item) if isinstance(item, dict) else {} title = ( content.get("title") or content.get("headline") or item.get("title") or "" ) publisher = ( (content.get("provider") or {}).get("displayName") or content.get("publisher") or item.get("publisher") or "" ) canon = content.get("canonicalUrl") or {} click = content.get("clickThroughUrl") or {} link = ( canon.get("url") or click.get("url") or content.get("link") or item.get("link") or "" ) headlines.append({ "title": title, "publisher": publisher, "link": link, "published": published, }) ``` The corresponding Skill instructions require the agent to consume this remote content: ```markdown Returns JSON with: ticker, headline_count, and headlines array (title, publisher, link, published date). **You must read these headlines and provide your own sentiment assessment** ``` ### ...[truncated 1915 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:13
Finding

Installation metadata permits an unpinned executable npm dependency

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/portfolio.py:141
Finding

Unvalidated numeric inputs allow portfolio cash and position corruption

Content
View full analysis
None: """Add a position to the portfolio.""" ticker = _normalize_ticker(ticker) data = _load(path) sector = get_sector(ticker) if sector is None: print(json.dumps({"error": f"Ticker {ticker} not found in FTSE 350 list"})) sys.exit(1) # Cost includes SDRT (stamp duty) total_cost = shares * price * (1 + RISK.sdrt_rate) if total_cost > data["cash"]: print(json.dumps({ "error": "Insufficient cash", "required": round(total_cost, 2), "available": round(data["cash"], 2), })) sys.exit(1) data["cash"] -= total_cost data["positions"].append({ "ticker": ticker, "shares": shares, "entry_price": price, "entry_date": str(date.today()), "sector": sector, }) _save(data, path) ``` The command-line values are converted but not constrained: ```python if args.init is not None: init_portfolio(args.init, path) elif args.add: ticker, shares_str, price_str = args.add add_position(ticker, int(shares_str), float(price_str), path) ``` ### Technical Analysis The code accepts negative, zero, and non-finite financial values. It does not enforce: - `capital > 0` - `shares > 0` - `price > 0` - Finite floating-point values - Valid ranges for loaded portfolio balances and position fields For negative shares and a positive price, `total_cost` becomes negative. The insufficient-cash check does not reject that value, and `data["cash"] -= total_cost` increases cash. Similar corruption is possible with negative prices. Values such as `NaN` can poison calculations and may be serialized as non-standard JSON tokens by Python’s default ...[truncated 948 chars]
Remediation
View remediation
0`. 2. Require shares to be a strictly positive integer. 3. Validate both command-line input and function-level input so direct callers cannot bypass checks. 4. Validate loaded portfolio files against a versioned schema before calculations or mutation. 5. Reject negative cash, invalid position fields, duplicate or malformed records, and unsupported JSON constants. 6. Use `json.dump(..., allow_nan=False)` to prevent `NaN` and infinities from being persisted. 7. Save atomically through a temporary file in the same directory followed by `Path.replace`. 8. Add tests for negative, zero, extremely large, `NaN`, and infinite values. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/risk.py:57
Finding

Mandatory risk validation fails open when ATR or portfolio data is unavailable

Content
View full analysis
float | None: """Fetch current ATR for a ticker. Returns None on failure.""" try: df = fetch_ohlcv(ticker, period="3mo", interval="1d") result = compute_indicators(df) return result["indicators"]["atr"] except Exception: return None ``` When ATR cannot be obtained, the skipped check is marked as passed: ```python else: stop_loss_price = None shares = math.floor(position_value / price) checks["risk_per_trade"] = { "pass": True, "detail": "ATR unavailable — skipped risk-per-trade check", } ``` Missing portfolio data is handled similarly: ```python else: checks["sector_exposure"] = { "pass": True, "detail": "No portfolio file — skipped", } checks["open_positions"] = { "pass": True, "detail": "No portfolio file — skipped", } checks["drawdown"] = { "pass": True, "detail": "No portfolio file — skipped", } all_pass = all(c["pass"] for c in checks.values()) ``` ### Technical Analysis A check that cannot be evaluated is represented as successful rather than unknown or failed. As a result, `all_pass` can be true even when the validator did not evaluate: - Risk per trade - Sector concentration - Open-position limits - Portfolio drawdown The ATR retrieval function also suppresses every exception, preventing callers from distinguishing a market-data outage from malformed data or an internal programming error. This behavior contradicts the Skill’s claim that its risk rules must never be overridden. ### Attack Path 1. Submit a proposed trade without `--portfolio-file`, or cause the referenced portfolio to be unavailable. 2. Cause or encounter a Yahoo Finance failure so ...[truncated 751 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/risk.py:221
Finding

Exposure validator is incompatible with the portfolio schema produced by the project

Content
View full analysis
None: """Create a new portfolio with the given starting capital.""" data = { "created": str(date.today()), "initial_capital": capital, "cash": capital, "positions": [], } _save(data, path) ``` Persisted positions also omit market value: ```python data["positions"].append({ "ticker": ticker, "shares": shares, "entry_price": price, "entry_date": str(date.today()), "sector": sector, }) ``` The exposure validator expects those missing fields and silently substitutes zero: ```python positions = portfolio.get("positions", []) total_value = portfolio.get("total_value", 0) peak_value = portfolio.get("peak_value", total_value) sector_values: dict[str, float] = {} for pos in positions: sector = pos.get("sector", "Unknown") val = pos.get("market_value", 0) sector_values[sector] = sector_values.get(sector, 0) + val sector_exposure = { sector: round(val / total_value * 100, 1) if total_value > 0 else 0 for sector, val in sector_values.items() } ``` ### Technical Analysis The producer and consumer use incompatible schemas. `portfolio.py` calculates market values only in memory when displaying a portfolio, while `risk.py` expects them to be persisted. Missing financial values are treated as zero rather than as schema errors. For a normal portfolio created using the documented commands: - `total_value` defaults to zero. - Every position’s `market_value` defaults to zero. - Sector exposure becomes zero percent. - `peak_value` also defaults to zero, preventing meaningful drawdown calculation. This ...[truncated 981 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/config.py:22
Finding

Documented mandatory daily-loss and diversification rules are not enforced

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (11)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

This mismatch is security-relevant because the skill is framed as an analysis agent, but it also includes portfolio CRUD and local bookkeeping operations that can alter stored user data. Hidden or underemphasized state-changing behavior raises the risk that a user authorizes an apparently analytical skill without understanding it can create, change, or delete portfolio records.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

This mismatch is security-relevant because the skill is framed as an analysis agent, but it also includes portfolio CRUD and local bookkeeping operations that can alter stored user data. Hidden or underemphasized state-changing behavior raises the risk that a user authorizes an apparently analytical skill without understanding it can create, change, or delete portfolio records.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

This mismatch is security-relevant because the skill is framed as an analysis agent, but it also includes portfolio CRUD and local bookkeeping operations that can alter stored user data. Hidden or underemphasized state-changing behavior raises the risk that a user authorizes an apparently analytical skill without understanding it can create, change, or delete portfolio records.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

This mismatch is security-relevant because the skill is framed as an analysis agent, but it also includes portfolio CRUD and local bookkeeping operations that can alter stored user data. Hidden or underemphasized state-changing behavior raises the risk that a user authorizes an apparently analytical skill without understanding it can create, change, or delete portfolio records.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

This mismatch is security-relevant because the skill is framed as an analysis agent, but it also includes portfolio CRUD and local bookkeeping operations that can alter stored user data. Hidden or underemphasized state-changing behavior raises the risk that a user authorizes an apparently analytical skill without understanding it can create, change, or delete portfolio records.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
83% confidence
Finding

The skill advertises scripts that can modify local state (portfolio.py --init/--add/--remove) and explicitly stores data in data/portfolio.json, but the manifest declares no tool scope or permissions boundary. That creates an authorization/expectation gap: a host or reviewer cannot tell from metadata that the skill may write files, increasing the chance of unintended data modification when the skill is installed or invoked.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The file’s module docstring explicitly describes a 'Portfolio tracker with CRUD operations' that creates, updates, and deletes holdings in a local JSON portfolio. The manifest describes a trading analysis agent focused on screening FTSE 350 stocks, news sentiment, trade recommendations, risk management, and backtesting, but does not mention maintaining or mutating a user portfolio datastore.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The _save function persists portfolio data to disk, and it is used by init_portfolio, add_position, and remove_position to create and modify state. The manifest emphasizes analytical capabilities and recommendations rather than persistent file-backed account/portfolio management, so this is a semantic expansion of behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
89% confidence
Finding

init_portfolio overwrites the target JSON file unconditionally, which can destroy existing portfolio records if the default path or a user-provided path already exists. In a trading context, loss of historical positions and cash records can materially affect decisions and auditability even though this is primarily an integrity/availability issue rather than code execution.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The portfolio commands include mutating actions (--init, --add, --remove) and the file states that positions are stored in data/portfolio.json, but there is no explicit warning near those commands that invoking them changes persisted user data. Users may reasonably interpret the skill as analytical/read-only and accidentally overwrite or alter portfolio state.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

This code file performs an outbound Yahoo Finance request via yfinance when fetching OHLCV data. While the function has an internal docstring, there is no user-facing notice in the CLI help, output, or comments explaining that running the script will contact an external service and transmit the requested ticker/query parameters.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.