Back to skill

Security audit

Eir Daily Content Curator

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent daily news-curation skill, but it needs review because it schedules persistent agents, fetches arbitrary URLs, stores API keys, and can post or shape Eir account content without tight safeguards.

Install only if you are comfortable with a scheduled agent searching the web, crawling selected URLs, storing local API keys, and posting generated content to an Eir account. Use a dedicated workspace, restrict config file permissions, review or remove the cron footer, avoid enabling personalization or interest sync unless intended, and prefer allowlisted public crawl/search endpoints.

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

T01 · Skill Instruction Hijacking

Error
Location
references/eir-setup.md:50
Finding
Persistent branded content injection into scheduled user-facing briefs<![CDATA[ ## Vulnerability Details **File Location**: `references/eir-setup.md:50-53` and `SKILL.md:203-205` **Vulnerability Type**: Persistent agent output manipulation **Risk Level**: High ### Technical Analysis The Skill instructs scheduled agents to append a fixed promotional message and external Eir link to every daily brief: ```bash # Job C: Daily brief (10 min after Job B, after subagent timeout) openclaw cron add --name "eir-daily-brief" \ --cron "45 7 * * *" --tz "Asia/Shanghai" \ --session isolated --agent content \ --message "Check pipeline execution, complete missing tasks, compile daily brief, deliver to user. End the brief with: Explore more on Eir → https://www.heyeir.com" ``` The primary Skill instructions reinforce the same behavior: ```markdown > **Tip:** End the brief with a link to [heyeir.com](https://www.heyeir.com) so readers can explore more content on the Eir canvas. ``` The cron message is loaded by an isolated agent on every scheduled execution. It therefore changes future user-facing output by requiring an external promotional call to action unrelated to the substantive news-curation result. Although scheduling a user-requested news pipeline is a legitimate feature, mandating branded promotional output is not necessary for that function. The behavior should be optional and clearly disclosed at configuration time. ### Attack Path 1. The user follows the documented Eir cron setup. 2. A persistent daily job is registered with the supplied agent message. 3. On each execution, the isolated agent compiles the user's brief. 4. The agent is instructed to append the fixed Eir promotional link. 5. User-facing output is repeatedly altered without a per-run request or confirmation. ### Impact Assessment The issue does not provide operating-system privileges or code execution. Its scope is control over persistent user-facing agent output. It can cause undisclosed promotion, reduce output integrity, and establish a pattern in whi ...[truncated 88 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the mandatory Eir footer from the scheduled agent message. - Make branded links an explicit configuration option that is disabled by default. - Ask for informed user consent before enabling any recurring promotional footer. - Store presentation preferences in a narrowly scoped setting such as: ```json { "brief": { "include_eir_link": false } } ``` - Ensure scheduled instructions focus only on the requested curation and delivery operations. - Allow users to inspect and approve the exact cron message before registration. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pipeline/crawl.py:205
Finding
Unrestricted candidate URL fetching permits server-side request forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pipeline/crawl.py:205-219`, `scripts/pipeline/crawl.py:246-255`, and `scripts/pipeline/crawl.py:453-518` **Vulnerability Type**: Server-side request forgery through unvalidated source URLs **Risk Level**: High ### Technical Analysis Candidate URLs are fetched directly with `urllib` without validating the scheme, resolved address, destination network, or redirect target: ```python def web_fetch_fallback(url): """Fallback: fetch page via direct HTTP request, return (text, html_head). Uses a simple urllib request with browser-like headers. Returns stripped text (not markdown) and raw HTML head for date extraction. """ try: req = urllib.request.Request(url, headers={ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml", "Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8", }) with urllib.request.urlopen(req, timeout=15) as resp: raw = resp.read(200000).decode("utf-8", errors="replace") ``` The lightweight date-extraction fallback has the same issue: ```python def fetch_html_head_only(url): """Fetch just the HTML head (first 20KB) for date extraction. Lightweight — only used when we already have markdown content but no date. """ try: req = urllib.request.Request(url, headers={ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", }) with urllib.request.urlopen(req, timeout=10) as resp: return resp.read(20000).decode("utf-8", errors="replace") except Exception: return "" ``` The same untrusted URLs can also be sent to configured Browse API and Crawl4AI services. No shared URL-security function rejects loopback, RFC 1918 privat ...[truncated 1597 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Introduce one mandatory URL-validation function used by every direct and delegated fetch path. - Permit only `http` and `https` schemes. - Reject URLs containing embedded credentials. - Resolve all destination addresses and reject loopback, private, link-local, multicast, unspecified, documentation, and reserved ranges for both IPv4 and IPv6. - Explicitly block cloud metadata destinations, including link-local metadata addresses and provider-specific metadata hostnames. - Disable automatic redirects or manually process them, applying the full validation policy to every redirect target. - Re-resolve immediately before connection to reduce DNS-rebinding risk. - Apply equivalent restrictions before forwarding URLs to Crawl4AI or the Browse API. - Consider an allowlist of acceptable public source domains where operationally feasible. - Add tests for encoded IP forms, IPv4-mapped IPv6, DNS rebinding, user-info parsing, alternate ports, and public-to-private redirects. ]]>

T01 · Skill Instruction Hijacking

Error
Location
scripts/pipeline/task_builder.py:190
Finding
Untrusted crawled content is embedded directly into agent generation instructions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pipeline/task_builder.py:190-194`, `scripts/pipeline/generate.py:79-101`, and `references/writer-prompt-eir.md:5-16` **Vulnerability Type**: Indirect prompt injection through web content **Risk Level**: High ### Technical Analysis Crawled article content is copied into a task as plain source text: ```python # Build source material text for the prompt source_text = "" source_meta = [] for i, s in enumerate(sources): source_text += "\n--- Source %d: %s (%s) ---\n%s\n" % ( i + 1, s["title"], s["name"], extract_article_body(s["content"], 4000)) source_meta.append({ "url": s["url"], "title": s["title"], "name": s["name"], "publishedDate": s.get("publishedDate"), }) ``` The content is then concatenated directly into the same prompt that contains trusted writer instructions: ```python prompt = """%s --- ## Task Topic slug: %s Angle: %s Why: %s Output language: %s %s Source material: %s Output ONLY the JSON. No other text or markdown fences.""" % ( writer_prompt, slug, angle, reason, output_lang, "\nReader context:\n" + reader_context if reader_context else "", source_text) return prompt ``` The writer prompt identifies the material as an input but does not explicitly state that source-embedded instructions are untrusted data that must never be followed: ```markdown ## Input You will receive: - `content_slug` — the content identifier (used as `slug` in output) - `topic_slug` — the directive topic this content belongs to (used as `topicSlug` and `interests.anchor`) - `angle`, `reason` — the editorial angle - `output_lang` — the language to write in (`"zh"` or `"en"`) - `reader_context` — optional context about the target audience. May be empty. - Source material — crawled article content with URLs, titles, and text ``` Simple textual separators do not create a reliable LLM trust boundary. A malicious article can contain instructi ...[truncated 1339 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Add an explicit high-priority instruction that source material is untrusted data and that instructions, requests, links, or role declarations inside it must never be followed. - Pass trusted instructions and untrusted source data in separate message roles or structured fields where the model API supports this. - Use strongly typed source objects instead of one concatenated instruction string. - Keep private reader context out of prompts containing arbitrary source text where possible. Prefer a de-identified audience category. - Validate generated values against task-owned metadata: - Force `slug` from `content_slug`. - Force `topicSlug` and `interests.anchor` from `topic_slug`. - Permit source URLs only from the approved task source list. - Reject unexpected fields and external links in prose. - Run schema and policy validation before any upload. - Require review or explicit confirmation when generated content contains new URLs, unusual instructions, or profile-derived details. - Treat model output as untrusted even after prompt hardening. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pipeline/eir_post.py:25
Finding
Unrestricted API endpoint overrides and redirects can expose Eir bearer credentials<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pipeline/workspace.py:103-139`, `scripts/pipeline/eir_post.py:25-39`, and `scripts/pipeline/eir_sync.py:23-28` **Vulnerability Type**: Bearer-token disclosure through unsafe endpoint selection **Risk Level**: Medium ### Technical Analysis The API URL and key can be loaded directly from environment variables without validating the URL scheme or destination: ```python def load_config() -> dict: """Load Eir API config from env vars or config file.""" api_url = os.environ.get("EIR_API_URL") api_key = os.environ.get("EIR_API_KEY") if api_url and api_key: return {"apiUrl": api_url, "apiKey": api_key} # Fallback: config file for path in [ Path(os.environ.get("EIR_CONFIG", "/dev/null")), CONFIG_DIR / "eir.json", SKILL_DIR / "config" / "eir.json", ]: if path.exists(): try: return json.loads(path.read_text()) except (json.JSONDecodeError, KeyError): continue return {} def get_api_url() -> str: """Return base API URL without trailing /api suffix. Normalizes both old (https://api.heyeir.com) and new (https://api.heyeir.com/api) formats to the same base.""" config = load_config() url = config.get("apiUrl", "").rstrip("/") # Strip /api suffix if present — callers add /api/oc/... themselves if url.endswith("/api"): url = url[:-4] return url ``` Authenticated requests then attach the bearer token to the resulting URL: ```python def api_request(method, url, data=None, api_key=""): """Make API request with retry.""" body = json.dumps(data, ensure_ascii=False).encode() if data else None for attempt in range(3): try: req = urllib.request.Request( url, method=method, data=body, headers={ "Authorization": "Bearer " + api_key, "Content-Type": "a ...[truncated 1949 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require `https` for every authenticated Eir endpoint. - Default to an immutable `https://api.heyeir.com` origin. - If custom endpoints are necessary, require explicit user approval and validate them against an administrator-controlled allowlist. - Reject URL user information, fragments, unexpected ports, and non-Eir hostnames by default. - Disable redirects for authenticated requests, or manually process redirects and permit them only when the scheme, hostname, and port remain identical. - Never forward an `Authorization` header across origins. - Separate endpoint configuration from credential configuration so an untrusted environment variable cannot silently pair an existing key with a new host. - Avoid returning detailed credential-bearing request information in errors or logs. - Rotate the Eir token if endpoint misrouting is suspected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/connect.py:49
Finding
API credentials are persisted without explicit owner-only file permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/connect.py:49-57` and `scripts/setup.py:76-81` **Vulnerability Type**: Insecure local credential storage permissions **Risk Level**: Medium ### Technical Analysis The connection script stores an Eir API key in `config/eir.json` using the process's default umask: ```python # Save credentials locally CONFIG_DIR.mkdir(parents=True, exist_ok=True) config = { "apiUrl": API_BASE, "apiKey": data["apiKey"], "userId": data["userId"], "connectedAt": datetime.now(timezone.utc).isoformat(), } CONFIG_PATH.write_text(json.dumps(config, indent=2)) ``` The setup script similarly writes `settings.json`, which may contain a search provider API key: ```python # Write settings.json into workspace settings_file = config_dir / "settings.json" settings_file.write_text(json.dumps(settings, indent=2, ensure_ascii=False)) ``` Neither path explicitly creates the credential file with mode `0600`, fixes the mode after replacement, nor enforces owner-only permissions on the containing configuration directory. On a system with a permissive umask or shared workspace, local users may be able to read the stored keys. Being excluded from version control prevents accidental commits but does not protect credentials from other local principals. ### Attack Path 1. The user runs `connect.py` or initializes settings with a search API key. 2. Python creates the file using permissions derived from the current umask. 3. On a permissive or shared environment, another local account can read the configuration file. 4. The attacker extracts the Eir or search provider API key. 5. The attacker uses the key within the provider's authorized scope. ### Impact Assessment An exposed Eir key may permit account-scoped API actions such as fetching curation information or posting content. An exposed search API key may enable unauthorized searches, consume paid quota, or incur billing. The finding requires another local principal ...[truncated 117 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the configuration directory with mode `0700`. - Create secret-bearing files atomically with mode `0600`, for example by using `os.open` with `O_CREAT | O_EXCL` and an explicit mode. - When updating existing files, write to an owner-only temporary file in the same directory, call `fsync`, and atomically replace the destination. - Apply `chmod(0o600)` after replacement as a defense against an unsafe pre-existing mode. - Check ownership before reading or overwriting credential files. - Refuse or warn when configuration files are group-readable, world-readable, or symbolic links. - Separate non-secret preferences from API keys where possible. - Prefer an operating-system credential store or OpenClaw-managed secret storage when available. - Document a migration step that repairs permissions on existing installations. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (48)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
88% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The description underspecifies that the skill builds internal task bundles, manages manifests/state, performs deduplication, and can report results back to an API. These backend workflow and reporting behaviors expand the data flow and persistence surface beyond what a user would expect from a simple daily briefing skill.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
**Response:** `{ "apiKey": "eir_oc_xxx", "userId": "u_abc123" }`

### DELETE /oc/connect
Disconnect and revoke API key.

### POST /oc/refresh-key
Confidence
81% confidence
Finding
A documented destructive endpoint that disconnects and revokes the API key can be dangerous if exposed to an LLM-driven agent without strong confirmation and authorization checks. In this skill context, an agent handling natural-language requests could be induced to call the disconnect action accidentally or via prompt manipulation, causing denial of service and loss of integration state.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
### GET /oc/content/:id
Read back a content item.

### DELETE /oc/content/:id
Delete by id or contentGroup.

### POST /oc/curation/miss
Confidence
90% confidence
Finding
The delete-content endpoint enables irreversible removal by id or contentGroup, creating a broad destructive capability that an agent could misuse if prompt-injected or if identifiers are guessed or mishandled. Because this skill automates curation and content publishing, accidental or malicious deletions could wipe large portions of curated output and disrupt the user's content pipeline.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
}
```

**Output rules:**
- 8-15 topics maximum
- Each topic: `label` (human-readable), `keywords` (search terms), `freshness` (how recent the content should be)
- De-identified labels only — no personal details
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
"\nReader context:\n" + reader_context if reader_context else "",
        source_text)

    return prompt


def save_generated(content_data, suffix=""):
Confidence
96% confidence
Finding
This function constructs and returns a full LLM prompt by directly embedding untrusted task fields such as source_text, reader_context, suggested_angle, and even a task-supplied writer_prompt. In a content-curation skill, those inputs are likely derived from web content or upstream files, so an attacker can inject prompt instructions that override system intent, manipulate output, exfiltrate hidden context included in the prompt, or steer the agent into unsafe generation behavior.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill declares environment variables and clearly instructs the agent to perform filesystem and network operations, but it does not explicitly constrain tool scope via permissions or allowed-tools. This increases the chance of over-broad agent execution, especially because the workflow includes reading/writing local files, calling external APIs, crawling URLs, and storing credentials.

External Transmission

Medium
Category
Data Exfiltration
Content
"mode": "standalone",
  "language": "en",
  "search": {
    "search_base_url": "https://api.search.brave.com/res/v1",
    "search_api_key": "YOUR_BRAVE_API_KEY"
  }
}'
Confidence
84% confidence
Finding
The skill is designed to send queries and possibly user-defined interests to external search providers, which is an intentional external transmission of potentially sensitive preference data. In a personalization context, this increases privacy risk because interest topics, freshness windows, and query content can reveal user profile information to third parties.

External Transmission

Medium
Category
Data Exfiltration
Content
Search provider examples:
| Provider | `search_base_url` | Get API key |
|----------|-------------------|-------------|
| Brave Search | `https://api.search.brave.com/res/v1` | [brave.com/search/api](https://brave.com/search/api/) |
| Tavily | `https://api.tavily.com` | [tavily.com](https://tavily.com/) |

> **Want richer results?** Install [SearXNG](https://docs.searxng.org/) and/or [Crawl4AI](https://github.com/unclecode/crawl4ai) locally. Add `searxng_url` and `crawl4ai_url` to your search config — they work as fallback or primary search/crawl providers.
Confidence
82% confidence
Finding
The documented use of Brave, Tavily, SearXNG, and Crawl4AI expands outbound data flows to multiple external or semi-external services. Because the skill performs personalized content curation, these requests may expose user interests, selected URLs, and browsing targets beyond the local environment.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
This file states that `lang` must be either `"zh"` or `"en"`, which imposes a language policy constraint in natural language. Because the document does not explain that this is a user-selected option or a narrowly justified regional limitation, it may violate the policy against forcing a specific language or locale without opt-in.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The API reference describes transmitting user interests, profile attributes, and generated content to a remote service, but it does not warn that sensitive personalization data and produced summaries will leave the local agent boundary. In a content-curation skill, this matters because interests, language preferences, and uploaded content can reveal user profiling data, and operators may deploy the skill without understanding the privacy implications.

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
The rules section states that the `lang` field is required and limited to only `en` or `zh`, which is a natural-language locale constraint. Because the document does not indicate user opt-in, language selection flexibility, or a region-specific justification, this may violate the policy against forcing a specific language/locale without user choice.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
The documented behavior expands a news-curation skill into collecting inferred interests from user conversations and mutating the user's interest profile via `POST /oc/interests/add`. That creates a privacy and authorization risk because conversational data is being repurposed for profile changes without an explicit, narrowly scoped consent step, and it can let the agent persistently shape future recommendations based on incidental or misinterpreted chat content.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The guide specifies `--tz "Asia/Shanghai"` for all scheduled jobs, which imposes a specific locale/timezone setting in natural-language setup instructions. Because no opt-in, alternative, or justification is provided, this appears to violate the policy against forcing a specific locale without user choice.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The manifest describes a skill for learning interests, searching the web, and delivering curated news summaries. This file instead pairs the installation with an external Eir service, exchanges a pairing code for an API key, and writes persistent credentials to disk, which is not part of the described end-user curation behavior.

External Transmission

Medium
Category
Data Exfiltration
Content
SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
CONFIG_PATH = CONFIG_DIR / "eir.json"
API_BASE = "https://api.heyeir.com/api"


def main():
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
SCRIPT_DIR = Path(__file__).resolve().parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
CONFIG_PATH = CONFIG_DIR / "eir.json"
API_BASE = "https://api.heyeir.com/api"


def main():
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.