Back to skill

Security audit

EPUB to Markdown

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it claims, but its overwrite mode can recursively delete an arbitrary resolved output directory if directed there.

Install only if you are comfortable running a local Python converter that writes files and may install dependencies through uv. Avoid using --overwrite unless the output path is definitely a disposable directory created by this tool.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/convert.py:149
Finding

Symlink-Aware Output Resolution Can Cause Unintended Recursive Directory Deletion

Content
View full analysis

Vulnerability Details

File Location: scripts/convert.py:149-159 and scripts/convert.py:385-388
Vulnerability Type: Unsafe recursive directory deletion
Risk Level: Medium

Vulnerable Code

python
def ensure_output_dir(output_dir: Path, overwrite: bool) -> None:
    if not output_dir.exists():
        return
    if not output_dir.is_dir():
        raise ValueError(f"Output path exists and is not a directory: {output_dir}")
    if not overwrite:
        raise ValueError(
            f"Output directory already exists: {output_dir}\\n"
            "Choose a different output directory or pass --overwrite to replace it."
        )
    shutil.rmtree(output_dir)

The user-controlled output path is resolved before it reaches the deletion operation:

python
output_dir = (
    Path(args.output_dir).expanduser().resolve()
    if args.output_dir
    else epub_file.with_suffix("")
)

Technical Analysis

The optional output path is accepted from the command line and canonicalized with Path.resolve(). Resolution follows symbolic links and produces the path of the link target. When --overwrite is enabled, ensure_output_dir() performs shutil.rmtree() directly on that resolved path.

The implementation verifies only that the resolved path is a directory. It does not:

  • Reject an output path that is a symbolic link.
  • Verify that the directory was previously generated by this converter.
  • Restrict deletion to a designated output root.
  • Protect sensitive paths such as a home directory, project directory, or filesystem root.
  • Request confirmation after displaying the canonical deletion target.

Consequently, a path that appears to identify an EPUB export directory may resolve to a different writable directory, which is then recursively removed.

Attack Path

  1. An attacker creates a symbolic link at a plausible output location, such as ./converted-book, poi ...[truncated 1310 chars]
Remediation
View remediation

Remediation Suggestions

  1. Check the original lexical output path for symbolic links before calling resolve() and reject any symlink component.
  2. Add a converter-specific marker file when creating output directories, then require and validate that marker before allowing recursive replacement.
  3. Refuse dangerous targets, including the filesystem root, user home directory, current project root, and the EPUB's containing directory.
  4. Display the canonical deletion target and require explicit confirmation in interactive use.
  5. Prefer deleting only known generated entries such as META.md, chapters/, and images/ rather than recursively removing an arbitrary directory.
  6. Prepare conversion output in a new temporary directory and replace the old generated export only after conversion succeeds.
  7. Where feasible, require the output directory to remain beneath an explicitly approved export root.

Example defensive logic should validate the unresolved path and marker before deletion:

python
def ensure_output_dir(output_dir: Path, overwrite: bool) -> None:
    if output_dir.is_symlink():
        raise ValueError("Refusing to use a symbolic link as the output directory")

    if not output_dir.exists():
        return
    if not output_dir.is_dir():
        raise ValueError("Output path is not a directory")
    if not overwrite:
        raise ValueError("Output directory already exists")

    marker = output_dir / ".epub-to-markdown-output"
    if not marker.is_file():
        raise ValueError("Refusing to delete an unverified output directory")

    shutil.rmtree(output_dir)

T08 · Insecure Dependencies

Note
Location
scripts/convert.py:7
Finding

Unbounded Dependency Versions Make Runtime Dependency Resolution Non-Reproducible

Content
View full analysis

Vulnerability Details

File Location: scripts/convert.py:7-14
Vulnerability Type: Unpinned automatically installed dependencies
Risk Level: Low

Vulnerable Code

python
# /// script
# requires-python = ">=3.10"
# dependencies = [
#   "ebooklib>=0.18",
#   "beautifulsoup4>=4.12",
#   "markdownify>=0.11",
# ]
# ///

The documented execution command causes uv to resolve these dependencies:

bash
uv run scripts/convert.py <path/to/book.epub> [output_dir]

Technical Analysis

Each dependency uses only a lower version bound. Therefore, future executions may install any later release accepted by the resolver rather than the versions that were present during this audit.

Although no malicious dependency or unsafe package index is identified in the audited files, this configuration weakens supply-chain integrity and reproducibility. A compromised future package release, compromised package-distribution account, or security-regressing update could be selected without any modification to the Skill package.

Dependencies execute with the same operating-system identity and filesystem access as the converter. Package import code executes before EPUB processing, making third-party packages part of the Skill's effective executable payload.

Attack Path

  1. A future release satisfying one of the broad constraints is published to the configured package index.
  2. The release is compromised, malicious, or contains a security regression.
  3. A user runs the documented uv run command in an environment without a previously locked dependency set.
  4. uv resolves and downloads the affected release.
  5. Python imports the installed package while starting the converter.
  6. Malicious import-time or runtime code executes with the permissions of the invoking user.

This is a prospective supply-chain exposure rather than evidence that the currently named packages are malicious.

Impa

...[truncated 549 chars]

Remediation
View remediation

Remediation Suggestions

  1. Pin every runtime dependency to an exact reviewed version.
  2. Maintain a lockfile containing cryptographic hashes for all direct and transitive dependencies.
  3. Execute with the locked dependency set rather than resolving new versions on every first run.
  4. Use a trusted, explicitly configured package index and disable unintended fallback indexes.
  5. Review and test dependency upgrades before updating pins.
  6. Run conversion in a sandbox with restricted filesystem and network access.
  7. Document that the first uv run may perform network access and install third-party code.

A basic improvement to the inline metadata would use exact versions:

python
# dependencies = [
#   "ebooklib==0.18",
#   "beautifulsoup4==4.12.3",
#   "markdownify==0.11.6",
# ]

Exact version pins should additionally be protected by reviewed package hashes or a checked-in lockfile.

Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (2)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill instructs the agent to run a local conversion script via uv run, which can write files and automatically fetch/install dependencies from the network, but it declares no permissions or allowed-tools scope. This creates an authorization gap: an agent may execute filesystem and network-capable actions without an explicit least-privilege boundary, increasing the risk of unintended file modification or supply-chain exposure from dependency resolution.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

When --overwrite is supplied, the script unconditionally calls shutil.rmtree(output_dir), which recursively deletes the entire target directory before recreating it. In an agent context, a user-provided or miscomputed output path could point to an unintended location, causing irreversible data loss without any interactive confirmation or safety boundary checks.

Content

No source excerpt is available for this finding.