Back to skill

Security audit

Media News Digest

Security checks for vulnerabilities and agentic risk

Overview

This skill is a plausible media news digest, but it needs Review because its network fetching and email delivery are under-scoped enough to expose users to unsafe URL and recipient handling.

Install only after reviewing and fixing the default sources and delivery settings. Use trusted workspace configs, correct or disable the bad Twitter handles, avoid --enrich unless outbound network access is sandboxed from private/internal addresses, and validate email recipients before enabling scheduled delivery. Treat Discord/email outputs as external sharing of generated content.

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 (5)

T01 · Skill Instruction Hijacking

Warning
Location
references/digest-prompt.md:106
Finding
Mandatory Promotional Content Alters Agent Output<![CDATA[ ## Vulnerability Details **File Location**: `references/digest-prompt.md:106-110` **Vulnerability Type**: Mandatory output manipulation **Risk Level**: Medium ### Vulnerable Code ```markdown ### Stats Footer ``` --- 📊 Data Sources: RSS {{rss}} | Twitter {{twitter}} | Reddit {{reddit}} | Web {{web}} | Dedup: {{merged}} articles 🤖 Generated by media-news-digest v<VERSION> | <https://github.com/draco-agent/media-news-digest> | Powered by OpenClaw ``` ``` ### Technical Analysis The Skill instructions require every generated digest to contain fixed branding and external promotional links. This requirement is unrelated to collecting, summarizing, or delivering news and prevents the caller from retaining complete control over the generated output. Because the behavior is embedded in the prompt loaded by the Agent, it changes the Agent's output requirements for the current session. Under the supplied classification criteria, mandatory promotional content constitutes Skill instruction hijacking even though it does not disable safety controls or execute code. ### Attack Path 1. The Agent loads `references/digest-prompt.md`. 2. The Agent follows the mandatory “Stats Footer” instructions. 3. Every generated digest receives the fixed project and OpenClaw attribution links. 4. The injected content is distributed to Discord or email recipients as part of the report. ### Impact Assessment The issue affects the integrity and neutrality of generated reports. It enables persistent insertion of third-party promotional links into user-facing output for every execution using this prompt. It does not grant operating-system privileges, expose credentials, or provide code execution. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Remove mandatory third-party links from the report-generation instructions. - Make attribution optional and controlled through an explicit configuration setting. - Default to no promotional footer unless the caller requests one. - Clearly separate operational report metadata from branding. - Ensure templates do not silently restore branding when the prompt omits it. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch-rss.py:273
Finding
Server-Side Request Forgery Through Configurable RSS Feed URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/config_loader.py:65-103`; `scripts/fetch-rss.py:273-297` **Vulnerability Type**: Server-Side Request Forgery **Risk Level**: High ### Vulnerable Code User-defined sources are accepted by the overlay loader: ```python config_path = config_dir / "media-news-digest-sources.json" # Try to load user overlay try: with open(config_path, 'r', encoding='utf-8') as f: config_data = json.load(f) user_sources = config_data.get("sources", []) logger.debug(f"Loaded {len(user_sources)} user sources from {config_path}") except FileNotFoundError: logger.debug(f"No user sources config found at {config_path}, using defaults only") return default_sources except json.JSONDecodeError as e: logger.warning(f"Invalid JSON in user sources config {config_path}: {e}, using defaults only") return default_sources # Merge logic: create lookup by id for efficient merging merged_sources = {} # Start with all default sources for source in default_sources: source_id = source.get("id") if source_id: merged_sources[source_id] = source.copy() # Apply user overlay for user_source in user_sources: source_id = user_source.get("id") if not source_id: continue if source_id in merged_sources: if user_source.get("enabled") is False: merged_sources[source_id]["enabled"] = False else: merged_sources[source_id] = user_source.copy() else: merged_sources[source_id] = user_source.copy() ``` The resulting URL is fetched without destination validation: ```python source_id = source["id"] name = source["name"] url = source["url"] priority = source["priority"] topics = source["topics"] for attempt in range(RETRY_COUNT + 1): try: global _rss_cache_dirty req_headers = {"User-Agent": "MediaDigest/2.0"} cache = _get_rss_cache(no_cache) cache_entry = cache.get(url) now = time.time( ...[truncated 2759 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Permit only `https` URLs and reject URLs containing credentials or malformed hostnames. - Resolve the hostname before connecting and reject all loopback, private, link-local, multicast, unspecified, and reserved IPv4 and IPv6 addresses. - Explicitly block cloud metadata destinations, including `169.254.169.254` and provider-specific metadata hostnames. - Disable automatic redirects or validate the scheme, hostname, and resolved address after every redirect. - Consider an allowlist of approved public RSS domains. - Apply the same validation immediately before connection to reduce DNS-rebinding risk. - Limit response size and accepted content types. - Run network fetchers in a sandbox with outbound firewall rules that deny private networks. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/enrich-articles.py:97
Finding
Server-Side Request Forgery Through Article Enrichment URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/enrich-articles.py:97-108,166-179` **Vulnerability Type**: Server-Side Request Forgery **Risk Level**: High ### Vulnerable Code ```python def fetch_full_text(url, max_chars=DEFAULT_MAX_CHARS): domain = get_domain(url) if domain in SKIP_DOMAINS: return {"text": "", "method": "skipped", "tokens": 0, "error": f"domain {domain} in skip list"} try: headers = {"Accept": "text/markdown, text/html;q=0.9", "User-Agent": USER_AGENT} req = Request(url, headers=headers) with urlopen(req, timeout=TIMEOUT) as resp: content_type = resp.headers.get("Content-Type", "") token_header = resp.headers.get("x-markdown-tokens", "") raw = resp.read() ``` Article-controlled links are submitted directly to the fetch function: ```python unique.sort(key=lambda x: -x.get("quality_score", 0)) to_fetch = unique[:max_articles] if not to_fetch: logging.info("No articles eligible for enrichment") return 0, 0, 0 logging.info(f"Enriching {len(to_fetch)} articles (min_score={min_score})") attempted = success = cf_count = 0 results = {} with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool: futures = {pool.submit(fetch_full_text, a["link"], max_chars): a["link"] for a in to_fetch} for future in as_completed(futures): url = futures[future] attempted += 1 result = future.result() ``` ### Technical Analysis When enrichment is enabled, article links originating from RSS feeds, search results, or supplied merged JSON are dereferenced. The existing `SKIP_DOMAINS` set only excludes a small number of public domains and is not an SSRF defense. The code does not validate the URL scheme, destination IP address, DNS result, or redirects. A malicious source can therefore publish a high-scoring article URL targeting an internal endpoint. DNS rebinding and public-to-private redirects can bypass simple hostname checks. ...[truncated 1255 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create a shared outbound URL validator and apply it to every article request. - Allow only HTTPS and reject URLs with embedded credentials. - Resolve and reject private, loopback, link-local, reserved, multicast, and unspecified IPv4 and IPv6 destinations. - Revalidate each redirect target and its resolved addresses. - Protect against DNS rebinding by validating the address used for the actual connection. - Enforce a strict maximum response size while streaming, rather than reading the entire response before truncation. - Require expected textual content types. - Consider limiting enrichment to domains already approved in source configuration. - Isolate enrichment in a sandbox whose firewall blocks internal and metadata networks. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send-email.py:54
Finding
Mailer Option Injection Through Unvalidated Recipient Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send-email.py:54-81,106-123` **Vulnerability Type**: Command option injection **Risk Level**: Medium ### Vulnerable Code ```python def send_via_msmtp(message: str, to_addrs: list) -> bool: """Send via msmtp (preferred).""" try: result = subprocess.run( ['msmtp', '--read-envelope-from'] + to_addrs, input=message.encode('utf-8'), capture_output=True, timeout=30 ) ``` ```python def send_via_sendmail(message: str, to_addrs: list) -> bool: """Send via sendmail (fallback).""" for cmd in ['sendmail', '/usr/sbin/sendmail']: try: result = subprocess.run( [cmd, '-t'] + to_addrs, input=message.encode('utf-8'), capture_output=True, timeout=30 ) ``` ```python parser.add_argument('--to', action='append', required=True, help='Recipient email (repeatable)') parser.add_argument('--subject', '-s', required=True, help='Email subject') parser.add_argument('--html', required=True, type=Path, help='HTML body file') parser.add_argument('--attach', type=Path, default=None, help='PDF attachment file') parser.add_argument('--from', dest='from_addr', default=None, help='From address') parser.add_argument('--verbose', '-v', action='store_true') args = parser.parse_args() # Expand comma-separated addresses to_addrs = [] for addr in args.to: to_addrs.extend([a.strip() for a in addr.split(',') if a.strip()]) ``` ### Technical Analysis Recipient strings are split but never validated as email addresses. They are appended directly to the argument arrays passed to `msmtp` and `sendmail`. A value beginning with `-` can be interpreted by the selected mail transport as an option instead of a recipient. This is not shell metacharacter injection because `subprocess.run` receives an argument list and does not enable a shell. The vulnerability is specific ...[truncated 1297 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse and validate every recipient with `email.utils.parseaddr` or a stricter mailbox parser. - Reject empty addresses, control characters, whitespace anomalies, and values beginning with `-`. - Validate that parsing consumes the complete input and returns a syntactically valid mailbox. - For `sendmail -t`, rely on validated MIME headers instead of appending recipient arguments. - For transports that require recipient arguments, insert `--` before recipients only where the transport explicitly supports it. - Maintain an explicit recipient count and length limit. - Validate `From` and `Subject` values against CR/LF injection even though the Python email package rejects many malformed header values. - Add tests covering leading-hyphen recipients and malformed mailbox syntax. ]]>

T08 · Insecure Dependencies

Note
Location
requirements.txt:5
Finding
Non-Reproducible Dependency Installation Due to Unbounded Versions<![CDATA[ ## Vulnerability Details **File Location**: `requirements.txt:5-8` **Vulnerability Type**: Unpinned third-party dependencies **Risk Level**: Low ### Vulnerable Code ```text # RSS parsing (optional, will fallback to regex if not available) feedparser>=6.0.0 # JSON Schema validation (optional) jsonschema>=4.0.0 ``` ### Technical Analysis The documented installation process permits any future version of `feedparser` and `jsonschema` satisfying the lower bounds. No upper bounds, exact versions, lock file, or package hashes are provided. No malicious package, typosquatting name, or currently compromised dependency was identified. The risk is that future installation results are not reproducible and may automatically consume a compromised or unexpectedly incompatible release without repository changes. The dependencies are documented as optional, which reduces exposure for deployments that do not install them. ### Attack Path 1. An operator runs `pip install -r requirements.txt`. 2. The package index resolves the newest versions satisfying the open-ended constraints. 3. A future compromised or malicious release satisfies those constraints. 4. The package is installed and later imported by the pipeline. 5. Malicious package code executes with the privileges of the Python process. This path requires a compromise or malicious future release of a named dependency; no such compromise was demonstrated during the audit. ### Impact Assessment If the upstream supply chain were compromised, package installation or import could execute code with the privileges of the user running the Skill. This could expose files, environment variables, API credentials, and network access available to that process. At present, this is a dependency-hardening weakness rather than evidence that either named package is malicious. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Pin dependencies to reviewed exact versions. - Generate a lock file for supported Python environments. - Require package hashes with `pip --require-hashes`. - Review and update pinned versions through a controlled dependency-update process. - Use a trusted package index and disable unapproved supplemental indexes. - Run dependency vulnerability and provenance checks in CI. - If the dependencies remain optional, document a standard-library-only installation path that does not install them. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
Findings (54)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The code’s behavior is materially narrower than the declared description. This script is a source-specific component for web search retrieval, not a complete media-industry digest system. Its core actions are selecting Brave/Tavily search backends, executing configured search queries, filtering matches, handling simple caching/rate limits, and outputting structured JSON. None of the declared downstream distribution capabilities or other source collectors are present in this chunk. While a single script in a larger pipeline need not implement every advertised feature, the prompt asks whether the supplied code chunk accurately matches the declared purpose; here, the chunk is significantly more limited and even self-describes as handling 'tech digest topics,' which further diverges from the media/entertainment description.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The chunk is broadly consistent with a digest-generation pipeline: it merges source outputs, scores quality, deduplicates, groups by topic, and emits structured output. It also supports Reddit as declared. However, the description specifically frames the system as a media & entertainment digest built from four sources (RSS, Twitter/X, Reddit, and web search). The code materially expands data collection/processing to additional undeclared sources: GitHub releases and GitHub trending repositories. Those are not merely implementation details; they are extra ingestion capabilities and inconsistent with the declared four-source scope. The code shown does not cover Discord/email/PDF output, but absence of unrelated pipeline stages in this chunk is not itself a mismatch. The key mismatch is undeclared GitHub-source processing and a source scope broader than described.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The code generally matches the idea of a pipeline-based news collection and merge process with deduplication/scoring, and it does use RSS, Twitter, Reddit, and web search as declared. However, it also runs fetch-github.py twice, including a GitHub Trending mode, which is not mentioned in the declared description and is materially inconsistent with a media & entertainment digest focused on Hollywood trades, box office, streaming, awards, festivals, and production news. The code shown does not demonstrate Discord/email/PDF output, but absence of those features in this chunk alone is not enough to call a mismatch. The main issue is the undeclared GitHub capability and the resulting broader/different source profile.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The code chunk’s behavior is materially narrower and different from the declared skill description. It acts as a support component for formatting a markdown digest into safe HTML, likely for email use, but does not implement the described end-to-end digest generation workflow. Because the declared purpose emphasizes multi-source news collection and output delivery features, while the actual code only sanitizes markdown into HTML, this is a description-behavior mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The declared description presents a full end-to-end media news digest system with multi-source ingestion, pipeline robustness features, and outbound delivery channels. The supplied code chunk does not implement those capabilities. Instead, it only loads an already-produced merged JSON file and prints a formatted summary of its contents, with sorting and filtering. While this could be a supporting component within a larger digest pipeline, the description does not accurately represent what this specific code chunk actually does, so this is a mismatch.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The code chunk does relate to a news/content aggregation pipeline, so it is not wholly unrelated. However, the declared description is specifically about generating media and entertainment industry digests from four sources: RSS, Twitter/X, Reddit, and web search. This script instead functions as a pipeline smoke test runner and validator. It invokes fetchers, validates JSON, and merges outputs; it does not generate final digests or send Discord/email/PDF outputs. More importantly, it includes a GitHub source type, which is not part of the declared four-source collection. The help text and examples also reference topics like llm, ai-agent, frontier-tech, and crypto, plus IDs such as sama-twitter and openai-rss, which suggests the configured domain may be tech/crypto rather than Hollywood/media-entertainment. Therefore the description does not accurately represent this code chunk's actual behavior.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
Lines L108-L109 explicitly state that the title, email heading, Discord heading, and email subject were unified to use a Chinese title. This is a natural-language locale policy concern because it imposes a specific language on user-facing output with no indication of user choice or documented regional justification in this file.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
This markdown file contains a natural-language policy constraint requiring all prompts, templates, comments, and code to be in English. The audit criteria flag language or locale policy violations when a specific language is forced without user opt-in or a clearly documented justification.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This markdown file instructs users to install the skill and deliver digests to Slack/Discord/email, and the document later notes the pipeline uses RSS, Twitter/X, and web search sources. Because the skill behavior includes external delivery and likely transmission of configured feeds or content to third-party channels, the README should disclose privacy and data-handling implications to users.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill advertises capabilities that involve environment variables, file I/O, network access, and shell execution, but it does not declare an explicit tool scope such as permissions or allowed-tools. This weakens least-privilege controls and makes it harder for a host agent to constrain what the skill may access, increasing the blast radius if the skill or its dependencies behave unexpectedly.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The documentation promotes email delivery with PDF attachments but does not warn that generated content may be transmitted to external recipients or infrastructure. In agent environments, this can lead to unintentional exfiltration of sensitive or proprietary content if users assume all processing stays local.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The configured Twitter handle value is a derogatory/offensive string that does not match the claimed trusted source. In a pipeline that fetches and summarizes external content, this can silently redirect collection to an unrelated or malicious account, poisoning downstream digests with disinformation, abusive content, or prompt-injection text from untrusted sources.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
This source entry uses a derogatory handle that is inconsistent with the named journalist, indicating either bad data hygiene or a potentially poisoned source mapping. Because the skill aggregates social content into outbound summaries, an incorrect handle can cause ingestion of attacker-controlled or irrelevant posts, leading to reputation harm, misinformation, and possible prompt-injection exposure through fetched content.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The default language placeholder is set to Chinese, and later rules require Chinese body text and writing the report in the specified language. This imposes a language choice by default without documenting user opt-in or offering alternatives, which matches the language/locale policy concern.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
This line explicitly mandates Chinese body text with English source links, which is a hard language constraint in the skill instructions. The file does not present this as a user-selected option or justify it as a region-specific requirement.

Natural-Language Policy Violations

Medium
Confidence
92% confidence
Finding
This instruction requires the report to be written in the value of <LANGUAGE>, and elsewhere the default is Chinese. Because the file does not indicate explicit user opt-in or a documented locale restriction, this is a natural-language policy issue.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The instruction "Chinese body + English links" imposes a specific language requirement in the template. Under the policy, forcing a language or locale without user opt-in or a clearly documented regional justification is a natural-language policy violation.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The template title is fixed in Chinese ("每日影视日报"), and the style guidelines later prescribe "Chinese body + English links," which indicates a forced language choice. Under the policy, language constraints should be opt-in or clearly justified as region-specific; this file does neither.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The instruction "Chinese body + English links" imposes a specific language format on generated content. This is a natural-language policy issue because it forces a locale/language behavior without presenting a user choice or explaining a justified regional scope.

External Transmission

Medium
Category
Data Exfiltration
Content
ID_CACHE_TTL_DAYS = 7

# Twitter API v2 endpoints
OFFICIAL_API_BASE = "https://api.x.com/2"
USER_LOOKUP_ENDPOINT = f"{OFFICIAL_API_BASE}/users/by"

# twitterapi.io endpoints
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
RETRY_DELAY = 2.0

# Brave Search API
BRAVE_API_BASE = "https://api.search.brave.com/res/v1/web/search"
TAVILY_API_BASE = "https://api.tavily.com/search"
BRAVE_RATE_LIMIT_CACHE = "/tmp/media-news-digest-brave-rate-limit.json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
# Brave Search API
BRAVE_API_BASE = "https://api.search.brave.com/res/v1/web/search"
TAVILY_API_BASE = "https://api.tavily.com/search"
BRAVE_RATE_LIMIT_CACHE = "/tmp/media-news-digest-brave-rate-limit.json"
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The Brave search parameters hard-code `search_lang` to `en` and `country` to `ALL`, which imposes an English-language preference on all searches. This is a natural-language policy concern because the script does not offer users a language choice or explain why English-only behavior is required.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The manifest explicitly frames the digest around media and entertainment news and names four collection sources: RSS feeds, Twitter/X KOLs, Reddit, and web search. This file adds separate GitHub releases and GitHub trending inputs, then incorporates them into scoring and final output, which expands behavior beyond the described source scope.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The manifest focuses on Hollywood trades, box office, streaming, awards, festivals, production news, and four named collection channels. Constructing digest entries from GitHub releases and trending repositories introduces a software-development/news capability that is not an obvious requirement for a media-industry digest.

Static analysis

No suspicious patterns detected.