T09 · Insecure Skill Coding Practices
- Location
scripts/morning-brief.py:257- Finding
Untrusted Feed Content Can Manipulate LLM Selection and Telegram Output
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill is a coherent AI news brief tool, but it automatically sends LLM-selected content to Telegram and broadly marks blogwatcher items read without clear user review or opt-in.
Review before installing if the configured Telegram destination is shared or public. Keep tokens in environment variables or a secret manager, confirm the API hub and ClawdChat services are acceptable third parties, and consider modifying the script to remove `blogwatcher read-all`, validate links, escape Telegram formatting, and require review before scheduled delivery.
scripts/morning-brief.py:257Untrusted Feed Content Can Manipulate LLM Selection and Telegram Output
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
method="POST"
)
try:
r = json.loads(urllib.request.urlopen(req, timeout=20).read())
if r.get("success"):
return r.get("data", {})
else:
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
req = urllib.request.Request(url, headers={
"User-Agent": "Mozilla/5.0 (compatible; RSS/1.0)",
})
resp = urllib.request.urlopen(req, timeout=15)
content = resp.read().decode("utf-8", errors="replace")
root = ET.fromstring(content)
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.
req = urllib.request.Request(url, headers={
"User-Agent": "Mozilla/5.0 (compatible; RSS/1.0)",
})
resp = urllib.request.urlopen(req, timeout=15)
content = resp.read().decode("utf-8", errors="replace")
root = ET.fromstring(content)
The script sends externally sourced article titles directly to a third-party LLM service, which creates an information disclosure boundary and exposes the decision logic to prompt-injection-by-content. In this skill context, many titles come from untrusted feeds and tool gateways, so attacker-controlled text can influence selection behavior or cause undesired downstream content to be published.
},
method="POST"
)
resp = urllib.request.urlopen(req, timeout=60)
result = json.loads(resp.read())
text = result["content"][0]["text"].strip()
The documented purpose does not fully match the described behavior: it references undeclared InfoQ access, performs blogwatcher read-all state-changing actions, and claims an OpenRouter snapshot is included even though it may only be silently read. This is dangerous because users may authorize the skill expecting passive content aggregation, while it also modifies external state and accesses sources not clearly disclosed.
The skill declares broad operational behavior involving environment variables, file access, networking, and shell execution, but does not define an explicit tool scope such as permissions or allowed-tools. This weakens least-privilege controls and makes it harder for users or the platform to constrain what the skill can access, increasing the blast radius if the skill or a downstream dependency behaves unexpectedly.
The documentation instructs sending collected content to Telegram but does not clearly warn that aggregated data will be transmitted to a third-party messaging service. Even if the content is mostly public news, summaries, metadata, or user-curated feeds can still reveal interests, subscriptions, and operational patterns, creating avoidable data leakage risk.
The setup instructions tell users to place bot tokens and API keys directly in script configuration without any security guidance. This encourages insecure secret handling practices, such as hardcoding credentials into files that may be checked into version control, copied into logs, or exposed to other local users.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
"""从 blogwatcher 获取最新未读文章"""
try:
# 先 scan
subprocess.run(["blogwatcher", "scan"], capture_output=True, timeout=60)
# 获取文章(默认显示未读)
result = subprocess.run(
["blogwatcher", "articles"],
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
# 先 scan
subprocess.run(["blogwatcher", "scan"], capture_output=True, timeout=60)
# 获取文章(默认显示未读)
result = subprocess.run(
["blogwatcher", "articles"],
capture_output=True, text=True, timeout=10
)
The prompt embeds untrusted article titles from RSS feeds, Reddit, Twitter, and tool-gateway responses directly into natural-language instructions for an LLM. Because LLMs can treat hostile content as instructions, an attacker can craft a title that biases ranking, injects misleading 'reason' text, or manipulates what gets posted to Telegram, making this more dangerous in an automated publish pipeline.
Collected article titles from multiple external sources are transmitted to a third-party LLM, and selected summaries are then pushed to Telegram, with no user-facing disclosure or approval step. In this skill's news-aggregation context the content is mostly public, so impact is not critical, but there is still a real privacy/data-governance risk and a chance of relaying malicious or unwanted content to downstream channels.
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.
"disable_web_page_preview": True,
}).encode()
req = urllib.request.Request(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
data=payload,
headers={"Content-Type": "application/json"},
method="POST"
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
def mark_all_read():
"""标记所有文章为已读"""
try:
subprocess.run(["blogwatcher", "read-all"], capture_output=True, timeout=10)
except Exception:
pass # blogwatcher 未安装时静默跳过
The inline comment explicitly states that calling get_openrouter_top_apps() will only update the cache, but the function actually opens an existing local file and returns parsed entries. This is an active contradiction between the comment and the code's real behavior, not just missing detail.
The natural-language instructions and operational guidance are presented only in Chinese, which effectively forces a specific language for users without opt-in. The file does not indicate that the skill is intentionally region-specific or provide an alternative language option.
The file-level description and all user-directed output behavior are written as a Chinese-only morning brief, with no indication that language is configurable or chosen by the user. Under the policy rule, forcing a specific language without opt-in is a natural-language locale policy violation.
The manifest says the skill includes an OpenRouter leaderboard snapshot as part of the briefing workflow, which implies the skill itself gathers that data. Here the function only reads a pre-populated local JSON cache and does not fetch the snapshot directly; this is a mild behavior mismatch between the described capability and the implemented operation.
No suspicious patterns detected.