Back to skill

Security audit

Video Downloader

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent video downloader, but its wrapper passes user-supplied URLs to yt-dlp without URL validation or an end-of-options marker, which can let option-like inputs change what yt-dlp does.

Review before installing. Use only trusted video URLs, avoid passing custom yt-dlp flags unless you understand them, and prefer a version that validates HTTP/HTTPS URLs and inserts '--' before the URL in every yt-dlp command.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
download_video.py:31
Finding
Untrusted URL Is Interpreted as yt-dlp Command-Line Options<![CDATA[ ## Vulnerability Details **File Location**: `download_video.py`, lines 31–59 **Vulnerability Type**: Command-line argument injection **Risk Level**: Medium ### Vulnerable Code ```python def inspect_video(url: str) -> dict: """Fetch metadata without downloading.""" result = subprocess.run( ["yt-dlp", "--dump-single-json", "--no-playlist", url], check=True, text=True, capture_output=True, ) return json.loads(result.stdout) def build_command(args: argparse.Namespace) -> list[str]: """Build the yt-dlp command from parsed arguments.""" output_dir = Path(args.output).expanduser() output_template = str(output_dir / "%(title)s [%(id)s].%(ext)s") cmd = ["yt-dlp", "--no-playlist", "--no-progress"] if args.audio_only: cmd.extend(["-x", "--audio-format", "mp3", "--audio-quality", "0"]) else: cmd.extend(["-f", format_selector(args.quality), "--merge-output-format", args.format]) if args.restrict_filenames: cmd.append("--restrict-filenames") cmd.extend(["-o", output_template, args.url]) return cmd ``` ### Technical Analysis The purported URL is appended directly to two `yt-dlp` command lines without an end-of-options delimiter such as `--`. A value beginning with `-` can therefore be parsed by `yt-dlp` as an option rather than as a media URL. Python's `subprocess.run` is correctly invoked with an argument list and without `shell=True`, so ordinary shell metacharacters do not produce shell injection. However, using a list does not prevent injection into the invoked program's own command-line parser. The wrapper's `argparse` configuration normally rejects an option-like positional value. Nevertheless, a caller can explicitly terminate the wrapper's options and pass such a value as the positional argument: ```bash python3 download_video.py -- '--batch-file=/tmp/attacker-list.txt' ``` The resulting metadata command is effectively: ```bash yt-dlp --d ...[truncated 2668 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Place an explicit end-of-options delimiter before every untrusted URL passed to yt-dlp: ```python result = subprocess.run( ["yt-dlp", "--dump-single-json", "--no-playlist", "--", url], check=True, text=True, capture_output=True, ) ``` Apply the same protection to the download command: ```python cmd.extend(["-o", output_template, "--", args.url]) ``` 2. Validate the URL before invoking yt-dlp. Permit only explicitly supported network schemes and require a hostname: ```python from urllib.parse import urlparse def validate_url(value: str) -> str: parsed = urlparse(value) if parsed.scheme not in {"https", "http"} or not parsed.hostname: raise ValueError("A valid HTTP or HTTPS video URL is required.") return value ``` 3. Explicitly reject values beginning with `-`, even after URL parsing, as defense in depth. 4. Consider adding `--ignore-config` to both yt-dlp invocations if ambient user or system yt-dlp configurations are not required. This prevents unrelated configuration files from silently changing the wrapper's reviewed behavior: ```python ["yt-dlp", "--ignore-config", "--dump-single-json", "--no-playlist", "--", url] ``` 5. Add regression tests covering: - `--batch-file=...` - `--config-locations=...` - `--exec=...` - malformed URLs - unsupported schemes - valid HTTP and HTTPS URLs 6. Continue using argument-list subprocess execution with `shell=False`; do not replace it with a shell command string. ]]>
Vulnerability Patterns
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

Lp3

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding
The skill clearly instructs use of shell-capable tooling (`python3 download_video.py` and fallback to raw `yt-dlp`) but does not declare any explicit tool scope such as `permissions` or `allowed-tools`. That creates an enforcement gap: an agent may invoke broader shell access than intended, increasing the risk of command misuse or unauthorized local actions if later prompts, wrapper behavior, or related files are adversarial or unsafe.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def ensure_yt_dlp() -> bool:
    """Return True when yt-dlp is available in PATH."""
    try:
        subprocess.run(
            ["yt-dlp", "--version"],
            check=True,
            stdout=subprocess.DEVNULL,
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 inspect_video(url: str) -> dict:
    """Fetch metadata without downloading."""
    result = subprocess.run(
        ["yt-dlp", "--dump-single-json", "--no-playlist", url],
        check=True,
        text=True,
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
print("Running:", " ".join(command))

    try:
        subprocess.run(command, check=True)
    except subprocess.CalledProcessError as err:
        print(f"Error: download failed with code {err.returncode}.", file=sys.stderr)
        return err.returncode
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Low
Confidence
87% confidence
Finding
This markdown file instructs the agent to store preferences in a local memory file, which is a file-write operation affecting user data. While the prompt asks for consent to remember behavior, it does not explicitly warn that this will modify a local file or describe that persistence side effect.

Static analysis

No suspicious patterns detected.