Back to skill

Security audit

calibre-catalog-read

Security checks for vulnerabilities and agentic risk

Overview

This Calibre skill has a legitimate catalog and analysis purpose, but it needs Review because it handles credentials broadly, probes alternate hosts automatically, and writes persistent analysis into the library with weak validation.

Install only if you are comfortable granting access to your Calibre server, book contents, local state storage, and comments metadata writes. Use a dedicated low-privilege Calibre account, keep unrelated secrets out of the workspace and ~/.openclaw .env files, avoid automatic host failover unless you trust every candidate host, and review generated analysis before allowing it to be written to comments.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/calibredb_read.mjs:306
Finding
Calibre Password Exposed in Process Arguments and Error Messages<![CDATA[ ## Vulnerability Details **File Location**: `scripts/calibredb_read.mjs:306-314`; `scripts/run_analysis_pipeline.py:8-12, 191-202` **Vulnerability Type**: Plaintext credential exposure **Risk Level**: High ### Vulnerable Code ```javascript function commonArgs(args) { const r = ['--with-library', String(args['with-library'] || '')]; const auth = args.__resolved_auth || resolveAuth(args); if (auth.username) r.push('--username', auth.username); if (auth.password) r.push('--password', auth.password); return r; } function run(cmd) { const cp = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' }); if (cp.status !== 0) { throw new Error(`calibredb failed (${cp.status})\nCMD: ${cmd.map(x => JSON.stringify(x)).join(' ')}\nERR:\n${(cp.stderr || '').trim()}`); } return cp.stdout || ''; } ``` The Python pipeline has the same issue: ```python def run(cmd: list[str]) -> str: cp = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if cp.returncode != 0: raise RuntimeError(f"cmd failed ({cp.returncode}): {' '.join(cmd)}\n{cp.stderr}") return cp.stdout ``` ```python pw = os.environ.get(ns.password_env, "") auth = [] if ns.username: auth += ["--username", ns.username] if pw: auth += ["--password", pw] rows = json.loads(run([ "calibredb", "--with-library", ns.with_library, *auth, "list", "--for-machine", "--search", f"id:{ns.book_id}", "--fields", "id,title,tags,formats", "--limit", "2" ])) ``` ### Technical Analysis The Calibre password is copied from an environment variable into the child process argument vector as the value following `--password`. Command-line arguments may be visible to other local processes through operating-system process inspection facilities. More critically, both implementations include the complete command in failure messages. Any failed `calibredb` operation therefore causes the plaintext password to be copied into an exception. Tha ...[truncated 1385 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not include a password-bearing command in exceptions or logs. 2. Add a central redaction function that replaces the value after `--password` with `[REDACTED]` before formatting diagnostics. 3. Use a protected credential mechanism supported by Calibre instead of command-line arguments where possible. 4. If command-line authentication cannot be avoided, isolate the process and ensure process inspection is restricted. 5. Return only the executable name, exit status, and sanitized stderr in failures. 6. Apply the same redaction controls to both JavaScript and Python implementations. 7. Add automated tests that deliberately fail a command and assert that a known test password is absent from stdout, stderr, exceptions, and state files. 8. Rotate any password that may already have appeared in execution logs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/calibredb_read.mjs:121
Finding
Credentialed Probes Are Automatically Sent to Unverified Failover Hosts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/calibredb_read.mjs:121-132, 184-224, 254-257`; `scripts/handle_completion.mjs:124-135, 187-227, 265-268` **Vulnerability Type**: Authentication to untrusted derived network endpoints **Risk Level**: Medium ### Vulnerable Code ```javascript function discoverWslHostCandidates() { const out = []; try { const rc = '/etc/resolv.conf'; if (existsSync(rc)) { const txt = readFileSync(rc, 'utf8'); for (const line of txt.split(/\r?\n/)) { const m = line.match(/^\s*nameserver\s+([0-9a-fA-F:.]+)\s*$/); if (m && m[1]) out.push(m[1]); } } } catch {} return out; } ``` ```javascript const extraHosts = [ ...splitList(args['server-hosts']), ...SERVER_HOSTS_ENV_KEYS.flatMap(k => splitList(process.env[k])), ...discoverWslHostCandidates(), 'host.docker.internal', ] .map(normalizeHostToken) .filter(Boolean); for (const c of baseCandidates) { const normalized = normalizeWithLibrary(c.value, libraryId); expanded.push({ source: c.source, value: normalized }); if (!isHttpUrl(normalized)) continue; const p = parseHttpUrlParts(normalized); if (!p) continue; const curHost = normalizeHostToken(p.host); for (const h of extraHosts) { if (!h || h === curHost) continue; expanded.push({ source: `${c.source}:host=${h}`, value: replaceHttpHost(normalized, h) }); } } ``` ```javascript function probeRemoteLibrary(withLibrary, auth) { const cmd = [ 'calibredb', 'list', '--for-machine', '--fields', 'id', '--limit', '1', '--with-library', withLibrary ]; if (auth.username) cmd.push('--username', auth.username); if (auth.password) cmd.push('--password', auth.password); const cp = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' }); return { rc: cp.status || 0, out: cp.stdout || '', err: cp.stderr || '' }; } ``` ### Technical Analysis The Skill begins with an explicitly configured Calibre URL b ...[truncated 2227 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove automatic host substitution based on `/etc/resolv.conf` and `host.docker.internal`. 2. Restrict failover to an explicit, user-approved allowlist. 3. Do not attach credentials to endpoint-discovery probes. 4. First establish that a candidate is the intended Calibre service, then authenticate. 5. Prefer HTTPS with certificate and hostname validation. 6. Pin expected hostnames or certificates where feasible. 7. Require an explicit opt-in before trying a different host. 8. Reject loopback, link-local, metadata-service, multicast, and unexpected private-network destinations unless specifically authorized. 9. Record sanitized audit events indicating which approved endpoint was selected. 10. Consolidate the duplicated resolution logic so both scripts enforce the same trust policy. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/calibredb_read.mjs:42
Finding
Skill Imports Entire Credential Files Instead of Required Calibre Variables<![CDATA[ ## Vulnerability Details **File Location**: `scripts/calibredb_read.mjs:42-69`; `scripts/handle_completion.mjs:22-49` **Vulnerability Type**: Excessive secret-file access and environment propagation **Risk Level**: Medium ### Vulnerable Code ```javascript function loadDotEnvFile(envPath) { if (!existsSync(envPath)) return; const txt = readFileSync(envPath, 'utf8'); for (const line of txt.split(/\r?\n/)) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const m = t.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/); if (!m) continue; const key = m[1]; if ( process.env[key] != null && String(process.env[key]).trim() !== '' ) continue; process.env[key] = parseDotEnvValue(m[2]); } } function hydrateEnvFromDotEnv() { const candidates = [ join(process.cwd(), '.env'), join(os.homedir(), '.openclaw', '.env'), ]; const seen = new Set(); for (const p of candidates) { if (seen.has(p)) continue; seen.add(p); loadDotEnvFile(p); } } hydrateEnvFromDotEnv(); ``` The read wrapper subsequently starts child processes without a restricted environment: ```javascript function run(cmd) { const cp = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' }); if (cp.status !== 0) { throw new Error(`calibredb failed (${cp.status})\nCMD: ${cmd.map(x => JSON.stringify(x)).join(' ')}\nERR:\n${(cp.stderr || '').trim()}`); } return cp.stdout || ''; } ``` ### Technical Analysis The declared functionality requires a limited set of Calibre values, principally `CALIBRE_PASSWORD`, optional `CALIBRE_USERNAME`, and Calibre endpoint configuration. Instead of selecting those keys, both scripts parse every assignment from the current workspace `.env` and the shared `~/.openclaw/.env`, then add every value to `process.env`. This gives the Skill access to unrelated credentials that may be stored in the same files, such as cloud API keys, database passwords, signing tokens, or credentials ...[truncated 1436 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse only an explicit allowlist: - `CALIBRE_PASSWORD` - `CALIBRE_USERNAME` - `CALIBRE_WITH_LIBRARY` - `CALIBRE_LIBRARY_URL` - `CALIBRE_CONTENT_SERVER_URL` - `CALIBRE_LIBRARY_ID` - `CALIBRE_SERVER_HOSTS` 2. Do not copy unrelated `.env` entries into `process.env`. 3. Prefer a dedicated Calibre configuration file containing only Calibre settings. 4. Start every child process with a minimal explicit environment. 5. Preserve only necessary runtime variables such as `PATH`, locale settings, and required Calibre values. 6. Check credential-file ownership and permissions before reading it; reject insecure permissions where supported. 7. Document the exact files and keys the Skill reads. 8. Add tests proving that unrelated sentinel variables from `.env` are unavailable to child processes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/run_analysis_pipeline.py:104
Finding
Unvalidated and Unescaped Subagent Output Is Written to Calibre HTML Comments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run_analysis_pipeline.py:104-146, 244-302`; validation claim at `SKILL.md:74` **Vulnerability Type**: Stored content injection and missing output-integrity validation **Risk Level**: Medium ### Vulnerable Code The pipeline only parses the supplied file as JSON: ```python if ns.analysis_json: analysis_core = json.loads(Path(ns.analysis_json).read_text()) else: is_fallback = True if not ns.allow_fallback: print(json.dumps({ "ok": True, "updated": False, "book_id": ns.book_id, "title": title, "file_hash": fhash, "analysis_mode": "fallback", "text_path": str(txt), }, ensure_ascii=False)) return analysis_core = simple_analysis(extracted, ns.lang) ``` Selected fields are then accepted without schema or identity validation: ```python record = { "book_id": ns.book_id, "library_id": ns.with_library.split("#", 1)[-1], "title": title, "format": ns.format, "file_hash": fhash, "lang": ns.lang, "summary": analysis_core["summary"], "highlights": analysis_core["highlights"], "reread": analysis_core["reread"], "tags": ["ai-summary", "cached-analysis"] + ( ["fallback"] if is_fallback else [] ), } ``` Dynamic values are interpolated into HTML without escaping: ```python lines = [ OC_START, '<div class="openclaw-analysis">', f"<h3>{tr['title']}</h3>" ] if summary: lines.append( f"<p><strong>{tr['summary']}:</strong> {summary}</p>" ) if highlights: lines.append(f"<h4>{tr['key_points']}</h4><ul>") for h in highlights: lines.append(f"<li>{h}</li>") lines.append("</ul>") ``` Reread fields and tags are handled in the same manner: ```python if parts: lines.append(f"<li>{' | '.join(parts)}</li>") meta_bits = [f"{tr['generated_at']}: {generated_at}"] if source_hash: meta_bits.append(f"{tr['file_hash']}: {source_ha ...[truncated 3214 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate every analysis file against `references/subagent-analysis.schema.json` in executable code before reading individual fields. 2. Reject output unless `analysis_core.book_id == ns.book_id`. 3. Reject output unless `analysis_core.lang == ns.lang`. 4. Enforce all string, array, object, length, and `additionalProperties` constraints. 5. Escape all dynamic text with an HTML-safe encoder before interpolation. 6. Prefer constructing HTML through a library that escapes text nodes by default. 7. Reject or strip tags, event attributes, script-capable URLs, and unsafe markup from all model-generated fields. 8. Bind completion artifacts to the stored run ID, book ID, source hash, and expected output path. 9. Treat ebook text as untrusted data in the subagent prompt and explicitly instruct the model to ignore instructions found in source content. 10. Validate after model completion even if structured output generation is enabled. 11. Add tests containing `<script>`, event-handler attributes, malformed objects, oversized values, and mismatched book IDs. 12. Avoid updating remote comments when any validation error occurs; mark the run failed and preserve a sanitized diagnostic only. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (31)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The documentation describes a read-focused catalog skill, but the workflow includes exporting book content, converting/reading full text, performing local fallback analysis, writing to local storage, and modifying Calibre comments metadata. This is a significant capability gap that can lead to unexpected data exfiltration, processing of sensitive book contents, and remote metadata modification under a misleading read-only label.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The documentation describes a read-focused catalog skill, but the workflow includes exporting book content, converting/reading full text, performing local fallback analysis, writing to local storage, and modifying Calibre comments metadata. This is a significant capability gap that can lead to unexpected data exfiltration, processing of sensitive book contents, and remote metadata modification under a misleading read-only label.

Ae1

High
Category
analysis-evasion
Content
2. Prepare input with `scripts/prepare_subagent_input.mjs`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
7. On completion, validate the raw JSON against `references/subagent-analysis.schema.json`, then run `scripts/handle_completion.mjs`.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
function hydrateEnvFromDotEnv() {
  const candidates = [
    join(process.cwd(), '.env'),
    join(os.homedir(), '.openclaw', '.env'),
  ];
  const seen = new Set();
Confidence
89% confidence
Finding
The code explicitly targets local .env files as a source of credentials and connection data, including a workspace .env path. Reading ambient secret stores from local disk is credential-access behavior that is broader than necessary for simple book catalog lookup and increases the chance of unintended secret collection.

Credential Access

High
Category
Privilege Escalation
Content
function hydrateEnvFromDotEnv() {
  const candidates = [
    join(process.cwd(), '.env'),
    join(os.homedir(), '.openclaw', '.env'),
  ];
  const seen = new Set();
  for (const p of candidates) {
Confidence
89% confidence
Finding
The code also targets a home-directory .env file under ~/.openclaw, broadening secret collection beyond the workspace into user-level configuration. This increases the blast radius because the skill may silently ingest long-lived credentials not intended for this specific operation.

Credential Access

High
Category
Privilege Escalation
Content
function hydrateEnvFromDotEnv() {
  const candidates = [
    path.resolve(process.cwd(), '.env'),
    path.join(os.homedir(), '.openclaw', '.env'),
  ];
  const seen = new Set();
Confidence
78% confidence
Finding
Reading `.env` from the current working directory can ingest secrets from an attacker-controlled or unintended project directory, causing credential confusion and expanding secret exposure to this process. In combination with later remote probing and apply behavior, this increases the chance of using the wrong credentials or accessing unintended services under ambient authority.

Credential Access

High
Category
Privilege Escalation
Content
function hydrateEnvFromDotEnv() {
  const candidates = [
    path.resolve(process.cwd(), '.env'),
    path.join(os.homedir(), '.openclaw', '.env'),
  ];
  const seen = new Set();
  for (const p of candidates) {
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Description-Behavior Mismatch

High
Confidence
97% confidence
Finding
The handler executes a delegated apply pipeline (`run_analysis_pipeline.py`) against a Calibre library and then removes state entries, which is a write-capable workflow inconsistent with a skill described as read-only catalog lookup/viewing. In a read-oriented skill, hidden mutation capability materially increases risk because a caller may trust it with broader access than intended, enabling unintended library modification if the state file or upstream workflow is influenced.

Description-Behavior Mismatch

High
Confidence
97% confidence
Finding
The script writes to both a local analysis database and Calibre comments metadata even though the skill is presented as catalog lookup/viewing plus analysis. That mismatch increases the chance of unauthorized or surprising state changes, and the metadata write injects persistent HTML into library records without an explicit consent step in this flow.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares executable capabilities that include environment access, file reads, and shell usage, but it does not define an explicit tool scope such as permissions or allowed-tools. That increases the risk of over-broad execution and makes it harder to constrain what the agent may access when handling untrusted prompts or book content.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The manifest frames the skill as read-only catalog lookup/viewing, yet the metadata explicitly declares remote modification of Calibre comments. This inconsistency can cause users or orchestrators to authorize the skill under false assumptions, leading to unintended writes to the library.

Intent-Code Divergence

Medium
Confidence
93% confidence
Finding
The routing guidance repeatedly labels the skill as read-only, while later instructions authorize a comments-apply workflow that writes metadata. Mixed safety signals create a real risk of accidental invocation in contexts where write operations should be prohibited.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The routing trigger allows use for broad 'natural book conversation' where a lightweight lookup helps, without strong boundaries for when the skill should or should not activate. In a skill that can access credentials, read local configuration, export content, and eventually write comments metadata, over-broad routing increases the chance of unnecessary sensitive operations or unintended tool use.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
Read the workspace `AGENTS.md` `## Tools` section for Content server URL, library id, auth policy, and reading script.

Connection bootstrap:
- Do not ask the user for `--with-library` first.
- First try scripts without explicit `--with-library`; they auto-load `.env` and saved defaults.
- Ask for URL only if resolution fails (`missing --with-library` or unable to resolve usable library).
- Non-SSL auth is Digest; do not pass auth-mode/auth-scheme flags.
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The `lang` field is constrained to `ja` or `en`, which imposes a locale/language restriction in the schema. Because this JSON manifest-like file does not document user choice, opt-in, or a justified region-specific limitation, it can violate the policy against forcing a specific language without user consent.

Natural-Language Policy Violations

Medium
Confidence
84% confidence
Finding
The natural-language-relevant `lang` field is constrained to `ja` and `en` only. In this file there is no accompanying explanation that the skill is region-specific or that users can opt into this restriction, which can violate language/locale policy requirements.

Context-Inappropriate Capability

Medium
Confidence
82% confidence
Finding
The schema persistently stores free-text `summary`, `highlights_json`, `reread_json`, `tags_json`, and indexes portions of that content in FTS for later search. For a catalog lookup/viewing skill, retaining searchable analysis text is more invasive than necessary and can expose sensitive or copyrighted derived content, while also creating a durable data store that other components may query unexpectedly.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The script does more than catalog lookup/viewing: `upsert()` creates schema and persists analysis records, summaries, highlights, reread notes, and tags. In a skill described as catalog lookup, viewing, and delegated one-book analysis, this broader write capability expands the trust boundary and allows persistent modification of local state beyond a read-only helper, which can be abused to store unauthorized content or alter downstream search results.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
The skill automatically loads connection settings and credentials from local .env files in the current working directory and the user's home directory, then injects them into process environment state. For a read-only catalog lookup tool, this broad ambient-secret ingestion exceeds least privilege and can unintentionally consume unrelated secrets or make sensitive settings available without explicit user consent.

Context-Inappropriate Capability

Medium
Confidence
87% confidence
Finding
The skill performs host discovery via /etc/resolv.conf parsing and then probes multiple alternate hosts, including host.docker.internal, when resolving the library endpoint. This expands the network reach of a nominal catalog-read skill and can trigger unintended connections to internal or environment-specific services.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
On subprocess failure, the thrown error includes the full calibredb command line, which may contain --username and --password arguments in cleartext. This can expose credentials to logs, calling agents, telemetry, or downstream error handlers, turning a routine failure path into a credential disclosure vector.

Context-Inappropriate Capability

Medium
Confidence
86% confidence
Finding
The skill hydrates environment variables from `.env` files and later uses credentials to probe remote Calibre servers, which exceeds the minimal privilege expected for a catalog-read skill. Even though it does not directly exfiltrate secrets here, automatically loading credentials and performing network probes broadens attack surface and can lead to credential misuse, unintended remote access, or SSRF-like internal connectivity attempts via attacker-influenced host settings.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The subprocess invocation hard-codes `--lang` to `ja` when no language is provided, which imposes a specific locale by default. This is a natural-language policy issue because users are not offered an explicit language choice before the skill selects Japanese.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The script sets `lang` to `'ja'` whenever `--lang` is omitted, which imposes a specific locale by default. This is a natural-language policy concern because the file does not offer an explicit user choice or justify why Japanese is required.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/calibredb_read.mjs:257

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/handle_completion.mjs:268