Back to skill

Security audit

coin-news-openclaw

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent crypto news collector with disclosed network fetching and local token-cache updates, with ordinary hardening gaps users should understand.

Before installing, be comfortable with the skill contacting multiple crypto news sites and CoinGecko, and with it updating its local scoring.yaml token cache unless --no-dynamic-tokens is used. Treat all fetched article content and links as untrusted, and avoid adding RSS sources you do not trust unless the runtime has network sandboxing.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_coin_news.py:153
Finding

Unrestricted Configurable URL Fetching Enables SSRF and Local Resource Access

Content
View full analysis
bytes: request = urllib.request.Request(url, headers={"User-Agent": user_agent}) with urllib.request.urlopen(request, timeout=timeout) as response: return response.read() ``` The unvalidated URL is obtained from the source configuration and passed directly to the network sink: ```python for source in sources["sources"]: if not source.get("enabled", True): continue try: feed_bytes = fetch_url( source["url"], sources["defaults"]["user_agent"], int(sources["defaults"]["request_timeout_seconds"]), ) ``` ### Technical Analysis Every enabled `source["url"]` from `references/sources.yaml` is passed directly to `urllib.request.urlopen`. The code does not enforce HTTPS, validate the hostname, restrict allowed ports, reject non-network URL schemes, resolve and inspect destination IP addresses, or validate redirect destinations. The committed configuration contains ordinary public HTTPS news feeds. Nevertheless, anyone capable of supplying or modifying the source configuration can cause the process to access destinations unrelated to cryptocurrency news, including loopback services, private network addresses, link-local addresses, or local resources supported by the URL handler. A timeout limits how long the request may run but does not constrain where it can connect. Redirects also need validation because an initially acceptable public URL could redirect to a prohibited destination. ### Attack Path 1. An attacker gains the ability to add or modify an enabled entry in `references/sources.yaml`, or influences an equivalent configuration supplied to the Skill. ...[truncated 1200 chars]
Remediation
View remediation

T01 · Skill Instruction Hijacking

Warning
Location
scripts/fetch_coin_news.py:158
Finding

Untrusted RSS Content Is Propagated into Agent-Facing Output Without Instruction Isolation

Content
View full analysis
list[Article]: root = ET.fromstring(feed_bytes) articles: list[Article] = [] for item in root.findall(".//item"): title = strip_html(item.findtext("title")) url = strip_html(item.findtext("link")) summary = strip_html(item.findtext("description")) published_at = parse_datetime(item.findtext("pubDate")) if not title or not url: continue articles.append( Article( source_id=source["id"], source_name=source["name"], source_weight=float(source.get("weight", 1.0)), category=source.get("category", "general"), title=title, url=url, summary=summary, published_at=published_at, ) ) ``` The content is then placed directly into Markdown: ```python # Markdown format with clickable links lines = ["# 📰 Coin News Digest\n"] for i, article in enumerate(articles, 1): title = article["title"] url = article["url"] source = article["source"] published = article.get("published_at", "")[:10] if article.get("published_at") else "" summary = article.get("summary", "")[:200] + "..." if len(article.get("summary", "")) > 200 else article.get("summary", "") tokens = ", ".join(article.get("matched_tokens", [])) topics = ", ".join(article.get("matched_topics", [])) lines.append(f"## {i}. [{title}]({url})") lines.append(f"**来源**: {source} | **时间**: {published} | **分数**: {article[' ...[truncated 2719 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/fetch_coin_news.py:153
Finding

Unbounded Network Response Reads Permit Memory and CPU Exhaustion

Content
View full analysis
dict[str, list[str]]: """Fetch top tokens from CoinGecko API (no API key required).""" url = COINGECKO_API_URL.format(limit=min(limit, 250)) request = urllib.request.Request( url, headers={"User-Agent": "FeedMob Coin News Collector/0.1"} ) with urllib.request.urlopen(request, timeout=timeout) as response: data = json.loads(response.read().decode("utf-8")) ``` RSS responses are also read completely and then parsed in memory: ```python def fetch_url(url: str, user_agent: str, timeout: int) -> bytes: request = urllib.request.Request(url, headers={"User-Agent": user_agent}) with urllib.request.urlopen(request, timeout=timeout) as response: return response.read() def strip_html(value: str | None) -> str: if not value: return "" value = re.sub(r"<[^>]+>", " ", value) return re.sub(r"\s+", " ", value).strip() def parse_datetime(value: str | None) -> dt.datetime | None: ``` The resulting response is parsed as one in-memory XML document: ```python def parse_feed(feed_bytes: bytes, source: dict) -> list[Article]: root = ET.fromstring(feed_bytes) ``` ### Technical Analysis Both network paths call `response.read()` without a maximum byte count. Request timeouts limit blocking time but do not impose a response-size limit. A server can return a very large body quickly enough to remain within the timeout. For RSS feeds, the entire response is first held as a byte string and then parsed into an XML object tree, increasing peak memory consumption. The configured `max_articles_per_source` limit ...[truncated 1225 chars]
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 (6)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill describes and encourages capabilities that read local files, write cached data, and access external networks, but it does not declare any explicit tool scope such as permissions or allowed-tools. That creates an under-specified trust boundary: a hosting agent may grant broader access than intended, making it easier for the skill to fetch remote content or modify local configuration without clear review.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
79% confidence
Finding

The skill makes an outbound network request to CoinGecko to retrieve token metadata, creating external data exposure and dependency on third-party content. In this context the transmission is expected for functionality, but it still expands the trust boundary and allows untrusted remote data to influence scoring and persisted state.

Content

Scanner excerpt · scripts/fetch_coin_news.py (reported line 29)May include surrounding context.

python
# Token fetch settings
TOKENS_TTL_HOURS = 24
COINGECKO_API_URL = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page={limit}"


@dataclass

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The script persists data fetched from an external service into the local scoring.yaml file, introducing side effects beyond a read-only news fetch/summarize workflow. This can unexpectedly alter future runs, poison local configuration if the upstream data is bad or manipulated, and violate least-surprise for an agent skill that appears operationally stateless.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

This code emits fixed Chinese-language field labels such as '来源' and '主题' in the generated markdown output. The file provides no language selection or opt-in mechanism, so it imposes a specific locale on all users.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The markdown example uses Chinese field labels/content (e.g. 来源, 时间, 分数, 主题) while the rest of the skill is primarily in English. Because the file does not state that output language is configurable or that the skill is intentionally Chinese-locale-specific, this can violate language/locale policy expectations.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The function documentation explicitly states that it preserves YAML structure, but the implementation serializes data anew with yaml.dump. That behavior can change comments, quoting, anchors, and potentially layout, so the documentation overstates what the code actually preserves.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.