Back to skill

Security audit

Sovereign Daily Digest

Security checks for vulnerabilities and agentic risk

Overview

This daily briefing skill appears purpose-built, but its script can collect private local data beyond the stated configuration and contains unsafe HTML-generation code that needs review before use.

Review this skill before installing. It should not be used on sensitive calendars, task files, or authenticated GitHub accounts until it enforces explicit source opt-in, fixes the Python path interpolation, escapes HTML output, and adds clear confirmations for scheduling, email sending, and archive deletion.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • 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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/digest.sh:584
Finding
Arbitrary Python Code Execution Through Output-Path Interpolation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/digest.sh:584-698` **Vulnerability Type**: User-controlled shell value interpolated into dynamically generated Python source **Risk Level**: High ### Vulnerable Code ```bash compile_html() { log "Compiling HTML digest..." local md_file="${OUTPUT_DIR}/digest-${TODAY}.md" local html_file="${OUTPUT_DIR}/digest-${TODAY}.html" # If markdown file doesn't exist yet, compile it first [[ ! -f "$md_file" ]] && compile_markdown # Convert markdown to HTML with inline styles python3 -c " import re, html as html_mod # ... with open('${md_file}', 'r') as f: md = f.read() body = md_to_html(md) # ... with open('${html_file}', 'w') as f: f.write(html) " 2>/dev/null } ``` `OUTPUT_DIR` can be supplied through the command line: ```bash --output) OUTPUT_DIR="$2" shift 2 ;; ``` ### Technical Analysis The `--output` argument is controlled by the caller and becomes part of `md_file` and `html_file`. These paths are then interpolated directly into the source text passed to `python3 -c`. Shell quoting around `"${OUTPUT_DIR}"` does not protect the Python source. A path containing a single quote and a valid Python expression can terminate or alter the Python string literal. When HTML generation runs, Python interprets the resulting value as source code rather than as data. For example, a malicious output path can alter an expression such as: ```python with open('${md_file}', 'r') as f: ``` so that a Python function call executes while Python evaluates the argument passed to `open()`. Execution can occur before the subsequent file operation fails. This is a source-code injection vulnerability. It is distinct from ordinary shell command injection because the affected interpreter is Python. ### Attack Path 1. An attacker gains the ability to influence arguments used to invoke `digest.sh`, including through an automation wrapper or scheduled invocation. 2. The atta ...[truncated 1119 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Never interpolate paths or other external values into interpreter source code. Pass the paths as ordinary arguments: ```bash python3 - "$md_file" "$html_file" <<'PY' import sys md_file = sys.argv[1] html_file = sys.argv[2] with open(md_file, "r", encoding="utf-8") as source: md = source.read() # Convert the document here. with open(html_file, "w", encoding="utf-8") as destination: destination.write(html) PY ``` Additional hardening should include: 1. Validate `OUTPUT_FORMAT` before running the converter. 2. Resolve output paths with a canonicalization function and enforce an intended output root where appropriate. 3. Reject paths containing NUL characters or other unsupported path data. 4. Avoid `python3 -c` for scripts containing dynamic values. 5. Add regression tests using spaces, quotes, newlines, Unicode, and shell metacharacters in output paths. 6. Do not suppress all Python errors with `2>/dev/null`; return a controlled diagnostic without exposing secrets. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/digest.sh:599
Finding
Stored HTML Injection Through Unescaped Digest Content<![CDATA[ ## Vulnerability Details **File Location**: `scripts/digest.sh:599-644` **Vulnerability Type**: Untrusted content inserted into HTML without contextual escaping or URL validation **Risk Level**: Medium ### Vulnerable Code ```python def md_to_html(md_text): lines = md_text.split('\n') out = [] in_table = False in_list = False for line in lines: stripped = line.strip() # Headings if stripped.startswith('# '): if in_list: out.append('</ul>'); in_list = False out.append(f'<h1>{stripped[2:]}</h1>') elif stripped.startswith('## '): if in_list: out.append('</ul>'); in_list = False out.append(f'<h2>{stripped[3:]}</h2>') elif stripped.startswith('### '): if in_list: out.append('</ul>'); in_list = False out.append(f'<h3>{stripped[4:]}</h3>') elif stripped.startswith('> '): out.append(f'<blockquote>{stripped[2:]}</blockquote>') elif stripped.startswith('---'): out.append('<hr>') elif stripped.startswith('- ') or stripped.startswith('* '): if not in_list: out.append('<ul>'); in_list = True out.append(f'<li>{stripped[2:]}</li>') elif stripped.startswith('|') and '|' in stripped[1:]: if '---' in stripped: continue cells = [c.strip() for c in stripped.split('|')[1:-1]] if not in_table: out.append('<table><tr>' + ''.join(f'<th>{c}</th>' for c in cells) + '</tr>') in_table = True else: out.append('<tr>' + ''.join(f'<td>{c}</td>' for c in cells) + '</tr>') elif stripped == '' and in_table: out.append('</table>') in_table = False elif stripped.startswith('*') and stripped.endswith('*') and not stripped.startswith('**'): out.append(f'<p><em>{stripped[1:-1]}</em></p>') elif stripped: ...[truncated 2785 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Replace the custom converter with a maintained Markdown implementation configured to disallow raw HTML, or apply strict contextual escaping. At minimum: 1. Escape all text before placing it in HTML: ```python from html import escape safe_heading = escape(stripped[2:], quote=True) out.append(f"<h1>{safe_heading}</h1>") ``` 2. Escape table cells, list entries, headings, blockquotes, task text, event summaries, feed titles, descriptions, and quote content. 3. Parse links separately and allow only approved schemes such as `https` and, if necessary, `http`. 4. Reject `javascript:`, `data:`, `file:`, and other unexpected schemes. 5. Escape attribute values with `quote=True`. 6. Add `rel="noopener noreferrer"` to links using `target="_blank"`. 7. Do not sanitize HTML with regular expressions. 8. Consider applying a sanitizer such as Bleach after Markdown conversion with a minimal element and attribute allowlist. 9. Add tests containing script tags, event handlers, encoded payloads, malformed tags, quote characters, and dangerous URL schemes. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/digest.sh:249
Finding
Configured Source Restrictions Are Ignored During Private Data Collection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/digest.sh:249-325` **Vulnerability Type**: Unconditional access to local calendars, task files, and authenticated GitHub data **Risk Level**: Medium ### Vulnerable Code ```bash # Check for local .ics files in common locations for ics_path in ~/calendars/*.ics ~/.local/share/calendars/**/*.ics; do if [[ -f "$ics_path" ]]; then local name name=$(basename "$ics_path" .ics) parse_ics "$(cat "$ics_path")" "$name" fi done ``` ```bash # todo.txt format local todotxt_path="${HOME}/todo.txt" if [[ -f "$todotxt_path" ]]; then while IFS= read -r line; do [[ -z "$line" || "$line" == x\ * ]] && continue COUNT_TASKS=$((COUNT_TASKS + 1)) local due_date due_date=$(echo "$line" | grep -oE 'due:[0-9]{4}-[0-9]{2}-[0-9]{2}' | sed 's/due://') if [[ -n "$due_date" ]]; then if [[ "$due_date" < "$TODAY" ]]; then echo "- **OVERDUE** ${line}" >> "$overdue_file" elif [[ "$due_date" == "$TODAY" ]]; then echo "- ${line}" >> "$today_file" else echo "- ${line}" >> "$other_file" fi else echo "- ${line}" >> "$other_file" fi done < "$todotxt_path" fi # Markdown task lists local md_tasks_path="${HOME}/tasks.md" if [[ -f "$md_tasks_path" ]]; then grep -E '^\s*- \[ \]' "$md_tasks_path" 2>/dev/null | while IFS= read -r line; do COUNT_TASKS=$((COUNT_TASKS + 1)) echo "${line}" >> "$other_file" done fi # GitHub issues (if gh CLI is available) if command -v gh &> /dev/null; then local gh_issues gh_issues=$(gh issue list --assignee @me --state open --limit 10 --json title,url,updatedAt 2>/dev/null || echo "") if [[ -n "$gh_issues" && "$gh_issues" != "[]" ]]; then echo "$gh_issues" | python3 -c " import sys, json try: issues = json.load(sys.stdin) for i in issues: print(f\"- [ ...[truncated 2633 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Enforce explicit source authorization before accessing private data: 1. Parse the configuration using a real YAML parser rather than the current flat `grep` helper. 2. Call each collection function only when its corresponding section is explicitly enabled. 3. Read only calendar and task paths listed in the active configuration. 4. Query GitHub only when a `github_issues` source is explicitly configured. 5. Require a repository value and pass it through `--repo` instead of querying the authenticated account globally. 6. On first use, display the exact local paths and external accounts that will be accessed and request confirmation. 7. Do not print the full digest to standard output by default when it contains private data; provide an explicit option for that behavior. 8. Create output files with restrictive permissions, such as mode `0600`, and output directories with mode `0700`. 9. Clearly warn users that archives contain copies of calendar, task, and account information. 10. Add tests proving that disabled sections perform no file reads or account queries. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • YARA SignaturesMalware Match, Webshell Match, Cryptominer Match
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (19)

YARA rule 'backdoor_persistence': Backdoor persistence with malicious payloads (shell commands, SSH key injection, hidden root users) [malware]

High
Category
YARA Match
Content
issing config, etc.).
- When the next scheduled digest will run (if scheduling is enabled).

---

## Scheduling

If the user asks to schedule the digest:

1. Parse the desired schedule into a cron expression.
2. Update the config file with the cron expression and `schedule.enabled: true`.
3. Create a crontab entry (Linux/macOS) or a scheduled task (Windows):

```bash
# Linux/macOS
(crontab -l 2>/dev/null; echo "${CRON} cd ~/.openclaw/daily-digest && bash scripts/digest.sh") | crontab -

# Windows (PowerShell)
# Provide instructions for Task Scheduler
```

4. Confirm the schedule to the user.

---

## Edge Cases and Error Handling

- **No internet:** Skip weather, RSS, and email. Generate digest from local sources only. Add a banner: "Generated in offline mode — some sections may be incomplete."
- **Empty sections:** Omit sections that have zero items rather than showing empty tables.
- **Large feeds:** Never process more than 50 items per feed. Truncate g
Confidence
75% confidence
Finding
YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

Vague Triggers

Medium
Confidence
87% confidence
Finding
The usage examples use generic activation phrases like "Give me my daily digest" and "Morning briefing with just calendar and tasks" without any explicit invocation constraints, exclusion conditions, or clarification about when this skill should activate. In a skill catalog or agent environment, these phrases are broad enough to collide with everyday requests for summaries or planning assistance.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
# Edit crontab
crontab -e

# Add this line for 7:00 AM every day
0 7 * * * cd ~/.openclaw/skills/daily-digest && bash scripts/digest.sh --format both
Confidence
85% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
### macOS (launchd)

Create `~/Library/LaunchAgents/com.openclaw.daily-digest.plist` with the appropriate schedule.

### Windows (Task Scheduler)
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The skill advertises broad trigger phrases such as 'daily report' and 'summarize my day' that can overlap with ordinary user requests, increasing the chance of unintended invocation. In a skill that reads local files, fetches network resources, writes output, and may alter scheduling, accidental activation can expose personal data or cause side effects without clear user intent.

Session Persistence

Medium
Category
Rogue Agent
Content
### Step 1: Load Configuration

1. Read `~/.openclaw/daily-digest/config.yaml`.
2. If the file is missing, create it with the defaults above, then inform the user: "I created a default config at `~/.openclaw/daily-digest/config.yaml`. Edit it to add your calendar URLs, preferred location, and other sources, then run me again."
3. Validate all fields. Warn (do not crash) on missing optional fields.

### Step 2: Determine Date Context
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The data-gathering steps direct the skill to contact external services such as wttr.in, RSS feeds, zenquotes.io, GitHub, and possibly IMAP, but the description does not clearly warn users that their configured locations, feed selections, repository identifiers, and potentially other metadata will be transmitted externally. This weakens informed consent and can lead to privacy exposure during normal use.

Session Persistence

Medium
Category
Rogue Agent
Content
### Step 6: Save Output

1. Create the output directory if it does not exist.
2. Save the file as `{output_dir}/digest-{YYYY-MM-DD}.md` and/or `.html`.
3. If `archive` is true, keep previous files up to `max_archive_days`, deleting older ones.
4. Print the full digest to stdout as well.
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The scheduling section instructs the skill to modify crontab or create scheduled tasks, which is a persistent system change, but it does not require a strong warning or explicit confirmation. Because this establishes recurring execution of a script that reads local data and performs network activity, users may unknowingly authorize long-lived automated behavior.

Session Persistence

Medium
Category
Rogue Agent
Content
```bash
# Linux/macOS
(crontab -l 2>/dev/null; echo "${CRON} cd ~/.openclaw/daily-digest && bash scripts/digest.sh") | crontab -

# Windows (PowerShell)
# Provide instructions for Task Scheduler
Confidence
95% confidence
Finding
This line installs a cron entry that repeatedly executes a local script, creating persistence on the host. While scheduling is an expected feature for a digest tool, persistence is still security-relevant because it causes recurring execution with access to local files, network endpoints, and potentially sensitive personal data.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The follow-up action 'Send this to [email]' enables transmission of digest contents to an external recipient without an accompanying warning that the digest may contain sensitive calendar, task, email, or news-derived information. This creates a realistic risk of accidental data exfiltration through user-friendly workflow text.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script makes automatic outbound requests to third-party services such as wttr.in, hnrss.org, TechCrunch, and zenquotes without explicit consent or clear disclosure. Even if the payload is small, it transmits user-linked metadata such as IP address and, for weather, user-supplied location, creating privacy risk in a personal-assistant context.

Session Persistence

Medium
Category
Rogue Agent
Content
log "Config not found. Creating default at $CONFIG_FILE"
        mkdir -p "$(dirname "$CONFIG_FILE")"
        cp "${SKILL_DIR}/SKILL.md" /dev/null 2>/dev/null || true
        # Write a minimal default config
        cat > "$CONFIG_FILE" << 'YAML'
general:
  timezone: "America/New_York"
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script reads sensitive local data sources including calendars, task files, and GitHub-assigned issues, then writes aggregated contents into persistent digest files. In a daily-digest skill, this can expose private schedules, tasks, and work items to other local users, backups, sync services, or downstream consumers of the generated output.

Intent-Code Divergence

Medium
Confidence
87% confidence
Finding
At L350 the user-facing message says task sources are configured via the config, implying task inputs come from configuration. However, the actual implementation in fetch_tasks reads only hard-coded paths like ~/todo.txt and ~/tasks.md and optionally queries GitHub issues, with no code that parses configured task sources from config.yaml.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script automatically deletes older digest markdown and HTML files using find -delete, which is a destructive operation. There is a log entry about cleaning archives, but no explicit warning in the help text or interface that running the script will remove prior outputs older than 30 days.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrases are generic productivity-language terms such as 'daily digest', 'daily report', and 'summarize my day', which are likely to match normal user requests rather than explicit invocation of this specific skill. This can cause the skill to activate unexpectedly and gain access to its required tooling or downstream workflows in contexts where the user did not intend to run it.

Missing User Warnings

Low
Confidence
78% confidence
Finding
The README mentions that first run creates a default configuration file and later describes output directories and scheduled execution, but it does not clearly warn users that running the skill will create and persist files on disk. For markdown files, user-facing documentation should disclose behavior that affects local data or system state.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The default configuration sets `language: "en"`, which establishes a specific language preference by default. The file does not indicate that the user is asked to choose a language initially or that this locale restriction is necessary for a region-specific purpose.

Static analysis

No suspicious patterns detected.