Back to skill

Security audit

Learning Coach

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches a learning-coach purpose, but its cron, network, install, and file-write helpers have enough unsafe scoping issues to require careful review before use.

Install only if you are comfortable with a skill that can create local learning records, fetch online resource feeds, and install persistent cron reminders. Review exact cron entries before applying them, avoid untrusted subject/workspace/source values, do not use the bootstrap --install option unless you accept global npm installation risk, and prefer running it in a dedicated workspace or user account until the scoping issues are fixed.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/setup_cron.py:25
Finding
Cron Command Injection Through an Unquoted Workspace Path<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup_cron.py:25-33, 40, 56-58` **Vulnerability Type**: Shell command injection through generated cron entries **Risk Level**: High ### Vulnerable Code ```python def default_jobs(workspace: Path) -> list[str]: py = "python3" base = workspace / "skills" / "learning-coach" / "scripts" weekly = base / "weekly_report.py" return [ f"30 7 * * * {py} {weekly} --mode daily-morning {TAG}", f"0 20 * * * {py} {weekly} --mode daily-evening {TAG}", f"0 10 * * 3,6 {py} {weekly} --mode curation-refresh {TAG}", f"0 7 * * 1 {py} {weekly} --mode weekly {TAG}", ] def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("action", choices=["apply", "remove", "show"]) ap.add_argument("--workspace", default=str(Path.home() / ".openclaw" / "workspace")) args = ap.parse_args() # ... jobs = default_jobs(Path(args.workspace)) merged = filtered + jobs set_crontab("\n".join([x for x in merged if x.strip()]) + "\n") ``` ### Technical Analysis The user-controlled `--workspace` value is interpolated directly into cron command strings without shell quoting or validation. Although the script does not invoke these command strings immediately, cron later executes each command through a shell. `pathlib.Path` does not neutralize shell metacharacters such as semicolons, command substitutions, redirection operators, or comment characters. Consequently, a crafted workspace value can change the structure of the scheduled shell command. The script also modifies the user's complete crontab through `crontab -`, making the injected command persistent across sessions until the entry is removed. ### Attack Path 1. An attacker causes the script to be invoked with a malicious workspace value, for example: ```bash python3 scripts/setup_cron.py apply \ --workspace '/tmp/fake; touch /tmp/learning-coach-pwned #' ``` 2. `default_jobs()` e ...[truncated 867 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve the workspace and require it to remain under an explicitly approved root: ```python approved_root = (Path.home() / ".openclaw" / "workspace").resolve() workspace = Path(args.workspace).expanduser().resolve() if workspace != approved_root: raise SystemExit("Unapproved workspace path") ``` 2. Quote every executable and path placed in a cron shell command: ```python import shlex command = ( f"{shlex.quote(sys.executable)} " f"{shlex.quote(str(weekly))} " "--mode daily-morning" ) ``` 3. Reject newline characters and other control characters in every value included in a crontab entry. 4. Prefer generating a fixed wrapper script at a trusted path and scheduling only that fixed script. 5. Before applying jobs, validate `data/cron-consent.json` and confirm that the exact commands and schedules match the user's approval. 6. Display the final escaped cron entries and require explicit confirmation before installation. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/source_ingest.py:15
Finding
Server-Side Request Forgery Through Arbitrary YouTube Feed URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/source_ingest.py:15-29, 76-80` **Vulnerability Type**: Server-side request forgery and unrestricted outbound request **Risk Level**: Medium ### Vulnerable Code ```python def fetch_text(url: str, timeout: int = 20) -> str: req = urllib.request.Request(url, headers={"User-Agent": "learning-coach/0.3"}) with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode("utf-8", errors="replace") def youtube_feed_url(channel: str) -> str: c = channel.strip() if c.startswith("http"): return c # Accept channel id or @handle; caller can pass full URL for custom resolution. if c.startswith("UC"): return f"https://www.youtube.com/feeds/videos.xml?channel_id={c}" return f"https://www.youtube.com/feeds/videos.xml?user={c}" # ... for ch in args.youtube: try: url = youtube_feed_url(ch) xml_text = fetch_text(url) candidates.extend(parse_youtube_rss(xml_text, args.limit_per_source)) ``` ### Technical Analysis Any `--youtube` value beginning with `http` is treated as a feed URL and passed to `urllib.request.urlopen()`. The implementation does not enforce an approved hostname, require HTTPS, inspect the resolved IP address, or validate redirect destinations. The process can therefore be induced to issue requests to loopback, private, link-local, or otherwise internal addresses. Redirect handling may also permit an initially public URL to redirect to an internal service. The fetched response is parsed as XML. This limits direct disclosure for endpoints that do not return compatible XML, but it does not prevent blind SSRF, interaction with internal HTTP services, or extraction from an internal endpoint that returns attacker-compatible Atom XML. ### Attack Path 1. An attacker supplies an internal URL through the repeatable `--youtube` option: ```bash python3 scripts/source_ingest.py \ --youtube 'http://127. ...[truncated 1162 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove support for arbitrary feed URLs unless it is essential to the declared functionality. 2. Allowlist expected YouTube feed hostnames and require HTTPS: ```python from urllib.parse import urlparse parsed = urlparse(url) if parsed.scheme != "https": raise ValueError("HTTPS is required") if parsed.hostname not in {"www.youtube.com", "youtube.com"}: raise ValueError("Unapproved feed host") ``` 3. Resolve the hostname and reject loopback, private, link-local, multicast, reserved, and unspecified addresses using `ipaddress`. 4. Disable automatic redirects or validate every redirect target with the same scheme, hostname, and resolved-address policy. 5. Set a maximum response size before reading the entire body to reduce denial-of-service risk. 6. Apply an outbound network policy that prevents the process from reaching cloud metadata endpoints and internal administrative networks. 7. Avoid including detailed internal network errors in persisted candidate output. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/update_progress.py:29
Finding
Subject Path Traversal Allows Writes Outside the Subject Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/update_progress.py:29-38, 95-96` **Vulnerability Type**: Path traversal and unintended file overwrite **Risk Level**: Medium ### Vulnerable Code ```python ap = argparse.ArgumentParser() ap.add_argument("--subject", required=True) ap.add_argument("--grading", required=True, help="Path to grading JSON (references/grading-schema.md)") ap.add_argument("--data-root", default="data") args = ap.parse_args() now = datetime.now(timezone.utc).isoformat() subject_slug = args.subject.strip().lower().replace(" ", "-") base = Path(args.data_root) / "subjects" / subject_slug base.mkdir(parents=True, exist_ok=True) progress_path = base / "progress.json" history_path = base / "quiz-history.json" # ... progress_path.write_text(json.dumps(progress, indent=2), encoding="utf-8") history_path.write_text(json.dumps(history, indent=2), encoding="utf-8") ``` ### Technical Analysis The `--subject` value is converted to lowercase and has spaces replaced with hyphens, but path separators and `..` components are retained. When this value is joined to `data-root/subjects`, traversal components can escape the intended subject storage directory. This implementation is inconsistent with `scripts/subject_store.py`, which applies a stricter regular-expression-based `slugify()` function. A subject initialized safely by one script may therefore be handled unsafely by `update_progress.py`. The names of the final files are fixed as `progress.json` and `quiz-history.json`, but the attacker controls the directory in which those files are created or overwritten. ### Attack Path 1. The attacker prepares a grading JSON file accepted by the script. 2. The attacker invokes the updater with traversal components in the subject: ```bash python3 scripts/update_progress.py \ --subject '../../../tmp/attacker-target' \ --grading /tmp/grading.json \ --data-root ./data ``` 3. The constructed path contains `data/subjec ...[truncated 809 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Move subject normalization into a shared utility and use the strict `slugify()` logic consistently across all scripts: ```python def slugify(value: str) -> str: value = value.strip().lower() value = re.sub(r"[^a-z0-9]+", "-", value) return re.sub(r"-+", "-", value).strip("-") or "subject" ``` 2. Resolve the destination and verify containment before creating directories: ```python subjects_root = (Path(args.data_root) / "subjects").resolve() base = (subjects_root / slugify(args.subject)).resolve() if base.parent != subjects_root: raise ValueError("Invalid subject path") ``` 3. Reject absolute paths, path separators, null bytes, `.` components, and `..` components in subject identifiers. 4. Use atomic writes through a temporary file in the validated destination followed by `os.replace()`. 5. Where existing files are not expected to be replaced, use exclusive creation or explicit authorization checks. 6. Add tests covering absolute paths, repeated traversal components, mixed separators, and encoded path separators. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/bootstrap.py:24
Finding
Unpinned Global npm Package Installation Creates Supply-Chain Risk<![CDATA[ ## Vulnerability Details **File Location**: `scripts/bootstrap.py:24-35, 40, 52-58` **Vulnerability Type**: Unpinned third-party dependency installation with global scope **Risk Level**: Medium ### Vulnerable Code ```python def run(cmd: list[str]) -> tuple[int, str]: p = subprocess.run(cmd, capture_output=True, text=True) return p.returncode, (p.stdout + p.stderr).strip() def maybe_install_clawhub() -> tuple[bool, str]: if has_bin("clawhub"): return True, "already installed" if not has_bin("npm"): return False, "npm not found" code, out = run(["npm", "i", "-g", "clawhub"]) return code == 0, out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--install", action="store_true", help="Attempt installation for missing optional deps") # ... if args.install: for c in checks: if c.name == "clawhub" and not c.found: ok, msg = maybe_install_clawhub() c.found = ok or has_bin("clawhub") install_results[c.name] = msg ``` ### Technical Analysis When `--install` is specified, the bootstrap script installs the latest registry version of `clawhub` using `npm i -g`. No exact version, integrity hash, lockfile, package provenance, or approved registry is specified. npm packages may execute lifecycle scripts during installation. A compromised package release, compromised registry account, malicious registry configuration, or dependency-chain compromise could therefore execute code during bootstrap. The `-g` option increases scope by modifying the global npm installation location rather than a project-local, isolated environment. The operation is explicit rather than hidden, but it exceeds the minimum scope needed for an optional dependency. ### Attack Path 1. The expected package or one of its transitive dependencies is compromised, or the environment is configured to use an untrusted npm registry. 2. A user runs: `` ...[truncated 916 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin the dependency to an explicitly reviewed version: ```python run(["npm", "install", "--global", "clawhub@<reviewed-version>"]) ``` 2. Verify package integrity and provenance against an approved digest or signed release. 3. Force use of an approved HTTPS registry rather than inheriting an arbitrary user or system registry configuration. 4. Prefer a project-local installation in an isolated directory instead of `-g`. 5. Use `--ignore-scripts` where package lifecycle scripts are not required: ```bash npm install --ignore-scripts --save-exact clawhub@<reviewed-version> ``` 6. Separate dependency checking from installation and display the exact version, registry, destination, and lifecycle-script policy before requesting confirmation. 7. Document that the optional package is not required for core learning-coach functionality and avoid automatic installation in unattended workflows. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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
Findings (18)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
89% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The skill is presented as a learning coach but also specifies external feed ingestion from YouTube/X/web, network fetching, and disk writes. That mismatch is security-relevant because it can conceal data-ingestion and egress behavior behind an educational label, increasing phishing, untrusted content ingestion, tracking, and SSRF-like risk depending on implementation.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises and instructs use of file I/O, shell, network access, and cron management, but declares no explicit tool scope or permission boundaries. That creates an over-privileged/under-specified execution model where a host may grant broad capabilities without user-visible constraints, increasing the risk of unintended file modification, network egress, or persistence operations.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd: list[str]) -> tuple[int, str]:
    p = subprocess.run(cmd, capture_output=True, text=True)
    return p.returncode, (p.stdout + p.stderr).strip()
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
The bootstrap script can perform a global npm installation of 'clawhub', which exceeds simple dependency checking and introduces a supply-chain and environment-modification capability. In the context of a learning-coach skill, silently or opportunistically installing global software is broader than necessary and could expose users to malicious packages, compromised registries, or unintended system-wide changes.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def get_crontab() -> str:
    p = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
    if p.returncode != 0:
        return ""
    return p.stdout
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def set_crontab(text: str) -> None:
    p = subprocess.run(["crontab", "-"], input=text, capture_output=True, text=True)
    if p.returncode != 0:
        raise RuntimeError(p.stderr.strip() or "failed to set crontab")
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
83% confidence
Finding
The manifest describes a learning coach focused on study planning, reminders, quizzes, grading, and adaptive roadmap updates. This file instead implements a generic ingestion pipeline that fetches YouTube RSS over the network, loads optional X/web feed JSON, and writes normalized content candidates, which is a broader content-harvesting behavior not reflected in the manifest description.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This markdown file states that `weekly_report.py` writes two files under `data/`, but it provides no warning or disclosure about modifying the local filesystem. For markdown files, missing warnings about behaviors that affect user data or system state should be flagged when the description omits such notice.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This Python code performs file creation and overwrite behavior by creating parent directories and writing a JSON output file, but it does not disclose that side effect through a prompt, warning comment, or descriptive message. The only visible output is the destination path, which does not clearly warn the user that data will be written under the subject directory or to a caller-supplied output path.

Missing User Warnings

Low
Confidence
79% confidence
Finding
This code writes a JSON file to a path derived from user input or a default subject directory, but the only visible disclosure is printing the output path after the write completes. There is no prior warning, confirmation, or explanatory comment/docstring near the write operation describing that the skill will create directories and overwrite intervention data.

Static analysis

No suspicious patterns detected.