Back to skill

Security audit

Douyin DL

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a genuine Douyin video downloader, but its script can execute local shell commands if given crafted URLs, filenames, output paths, or page-controlled video sources.

Review before installing. Do not run this skill on untrusted or pasted URLs, filenames, or output directories unless the script is fixed to avoid shell=True, validate Douyin domains and HTTPS media hosts structurally, sanitize explicit filenames, and pin or localize the agent-browser dependency.

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/douyin_download.py:114
Finding
OS Command Injection Through the Input URL<![CDATA[ ## Vulnerability Details **File Location**: `scripts/douyin_download.py`, lines 20-22, 65-66, and 114 **Vulnerability Type**: Shell command injection **Risk Level**: High ### Vulnerable Code ```python def run(cmd: str, timeout: int = 30) -> str: """Run a shell command and return stdout.""" r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) return r.stdout.strip() ``` ```python # Fallback: try as-is return url, None ``` ```python out = run_check(f"agent-browser open '{url}'", timeout=args.timeout) ``` The corresponding command execution helper also enables shell interpretation: ```python def run_check(cmd: str, timeout: int = 30) -> str: """Run a shell command and raise on failure.""" r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) if r.returncode != 0: raise RuntimeError(f"Command failed: {cmd}\n{r.stderr}") return r.stdout.strip() ``` ### Technical Analysis The positional URL argument is inserted into a shell command inside single quotes. The application attempts no shell escaping before passing the resulting string to `subprocess.run()` with `shell=True`. The URL normalization function also returns unsupported URLs unchanged. Consequently, a single quote in a supplied value can terminate the quoted argument. Shell metacharacters following that quote are then interpreted as commands by the operating-system shell. This is not limited to URLs from the documented Douyin domains because the fallback behavior accepts an arbitrary value as-is. ### Attack Path 1. An attacker causes the Skill to be invoked with a URL containing a single quote and shell syntax, such as a value conceptually shaped like: ```text https://example.invalid/x'; attacker_command; # ``` 2. `normalize_url()` does not recognize it as a supported Douyin URL and returns the entire value unchanged. 3. The program constructs a command resembling: ```sh agen ...[truncated 684 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Eliminate shell interpretation and pass each command argument separately: ```python def run_check(args: list[str], timeout: int = 30) -> str: result = subprocess.run( args, shell=False, capture_output=True, text=True, timeout=timeout, check=True, ) return result.stdout.strip() out = run_check(["agent-browser", "open", url], timeout=args.timeout) ``` In addition: 1. Require the URL scheme to be `https`. 2. Normalize the hostname using `urllib.parse`. 3. Allow only exact approved hosts, such as `www.douyin.com` and `v.douyin.com`, or correctly bounded subdomains if operationally required. 4. Reject URLs containing credentials, unsupported ports, control characters, or malformed hostnames. 5. Remove the arbitrary-URL fallback and fail closed when the input does not match a supported Douyin URL format. 6. Never rely on manually adding quotes to make untrusted data safe for a shell. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/douyin_download.py:138
Finding
OS Command Injection Through the Output Directory or Explicit Filename<![CDATA[ ## Vulnerability Details **File Location**: `scripts/douyin_download.py`, lines 138-143 and 150-159 **Vulnerability Type**: Shell command injection and unrestricted output-path selection **Risk Level**: High ### Vulnerable Code ```python if not args.filename: title_out = run("agent-browser get title") title = title_out.replace(' - 抖音', '').strip() filename = sanitize_filename(title) if title else f"douyin_{video_id or 'video'}" else: filename = args.filename ``` ```python output_path = os.path.join(args.output_dir, f"{filename}.mp4") os.makedirs(args.output_dir, exist_ok=True) print(f"⬇️ Downloading to: {output_path}") dl_cmd = ( f"curl -L -o '{output_path}' " f"-H 'Referer: https://www.douyin.com/' " f"-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' " f"'{video_src}'" ) r = subprocess.run(dl_cmd, shell=True, capture_output=True, text=True, timeout=300) ``` ### Technical Analysis Automatically extracted page titles are passed through `sanitize_filename()`, but a value supplied through `--filename` is assigned directly. The caller-controlled `--output-dir` value is also used without shell escaping. Both values contribute to `output_path`, which is embedded inside a single-quoted curl command. Because the command is executed with `shell=True`, a quote in either value can close the intended shell argument and introduce additional commands. The output directory is otherwise unrestricted. Even after command injection is removed, a caller can direct the downloaded content to any filesystem directory writable by the current user. ### Attack Path 1. An attacker controls the `--filename` or `--output-dir` argument. 2. The attacker includes a single quote followed by shell metacharacters and an operating-system command. 3. The program constructs `output_path` using the malicious value. 4. The path is interpolated into the string passed to `subprocess.run(..., shell=True)`. 5. The shel ...[truncated 809 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Invoke curl with an argument list and disable the shell: ```python subprocess.run( [ "curl", "-L", "-o", output_path, "-H", "Referer: https://www.douyin.com/", "-H", "User-Agent: Mozilla/5.0", video_src, ], shell=False, capture_output=True, text=True, timeout=300, check=True, ) ``` Apply filename controls consistently: ```python filename = sanitize_filename(args.filename) if args.filename else sanitize_filename(title) ``` Additional hardening should include: 1. Reject path separators and null or control characters in filenames. 2. Resolve the configured output directory and final path with `pathlib.Path.resolve()`. 3. If output must be confined to an approved directory, verify that the resolved final path is a descendant of that directory. 4. Reject absolute filenames and parent-directory components. 5. Avoid overwriting existing files unless explicitly authorized. 6. Create output files safely and consider protections against symbolic-link replacement when operating in directories writable by other users. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/douyin_download.py:79
Finding
Page-Controlled Video Source Is Passed to a Shell Command<![CDATA[ ## Vulnerability Details **File Location**: `scripts/douyin_download.py`, lines 65-66, 79-81, and 150-159 **Vulnerability Type**: Untrusted remote data used in a shell command **Risk Level**: High ### Vulnerable Code ```python # Fallback: try as-is return url, None ``` ```python for v in videos: src = v.get('currentSrc') or v.get('src') if src and ('douyinvod.com' in src or 'bytevcloudcdn.com' in src or '.mp4' in src): return src ``` ```python dl_cmd = ( f"curl -L -o '{output_path}' " f"-H 'Referer: https://www.douyin.com/' " f"-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' " f"'{video_src}'" ) r = subprocess.run(dl_cmd, shell=True, capture_output=True, text=True, timeout=300) ``` ### Technical Analysis The downloader can open arbitrary pages because unsupported input URLs are accepted unchanged. It then reads `currentSrc` or `src` from video elements in the remote page. The source validation uses substring matching. A source is accepted if it contains `douyinvod.com`, `bytevcloudcdn.com`, or `.mp4` anywhere in the string. This does not establish that the parsed hostname is an approved CDN host, nor does it require HTTPS. The accepted value is interpolated into a command string executed with `shell=True`. Therefore, remote page data reaches a shell-command sink without robust validation or shell-safe argument handling. ### Attack Path 1. A victim invokes the downloader with an attacker-controlled page because the URL fallback permits arbitrary destinations. 2. The page exposes a video element whose source satisfies the weak substring test, for example by containing `.mp4`. 3. The extractor returns the page-controlled source. 4. The source is inserted inside the curl command's single-quoted URL argument. 5. If the resulting browser-provided source contains shell-significant quoting that reaches the command unchanged, it can terminate the argument and append commands. 6. The s ...[truncated 672 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Remove the shell from the download operation by passing curl arguments as a list with `shell=False`, or use a maintained HTTP client library directly. Validate the extracted source structurally: 1. Parse it with `urllib.parse.urlsplit()`. 2. Require the `https` scheme. 3. Reject embedded credentials and unexpected ports. 4. Compare the normalized hostname against an explicit allowlist. 5. For subdomains, require an exact hostname or a properly bounded suffix such as `host == allowed` or `host.endswith("." + allowed)`. 6. Do not use substring tests such as `'douyinvod.com' in src`. 7. Reject malformed URLs, control characters, and non-HTTP schemes. 8. Restrict the initial page URL to approved Douyin hosts so arbitrary pages cannot provide the source. 9. Apply download-size and response-content-type limits before accepting the resulting file. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:16
Finding
Unpinned Globally Installed npm Dependency<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, line 16 **Vulnerability Type**: Unsafe and non-reproducible third-party dependency installation **Risk Level**: Medium ### Vulnerable Code ```markdown ## Prerequisites - `agent-browser` (`npm i -g agent-browser`) - `curl` ``` ### Technical Analysis The installation instruction retrieves the current version of `agent-browser` from the npm registry and installs it globally. No reviewed version, lockfile, or integrity constraint is supplied. npm packages can run lifecycle scripts during installation. Consequently, the effective code installed by this instruction can change after the Skill has been reviewed. Global installation also gives the package a broader and more persistent position in the user's executable environment than a project-local dependency. This finding concerns unsafe dependency handling; the audited files do not establish that the named package itself is malicious. ### Attack Path 1. A user follows the documented prerequisite command. 2. npm resolves the package version available under the package name at installation time. 3. npm downloads the package and its transitive dependencies. 4. Applicable package lifecycle scripts execute during installation. 5. If the package, maintainer account, release process, or a transitive dependency has been compromised, malicious code can execute in the installing user's context. 6. The globally installed executable remains available to later invocations of the Skill and other local workflows. ### Impact Assessment A compromised dependency could execute commands with the installing user's privileges, access user-readable data, alter user-owned files, or replace the behavior expected from `agent-browser`. The exact impact depends on the npm configuration and privileges used for installation. The instruction does not explicitly require root access, but users who run global npm installation with elevated privileges would increase the poten ...[truncated 17 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin `agent-browser` to a specific reviewed version rather than installing the latest release. 2. Prefer a project-local dependency recorded in `package.json` and a committed lockfile. 3. Use deterministic installation, such as `npm ci`, against the reviewed lockfile. 4. Verify registry provenance, package ownership, release signatures or attestations where available, and lockfile integrity. 5. Review transitive dependencies and lifecycle scripts. 6. Avoid global installation unless it is operationally necessary. 7. Do not recommend elevated installation privileges. 8. Document the exact tested version and update it only through a deliberate review process. ]]>
Vulnerability Patterns
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • 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
Findings (7)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
def run(cmd: str, timeout: int = 30) -> str:
    """Run a shell command and return stdout."""
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
    return r.stdout.strip()
Confidence
99% confidence
Finding
This is a true tool-parameter abuse issue because the helper normalizes command execution through a shell, making every caller vulnerable if any parameter is attacker-controlled. In this skill, inputs originate from a user-provided Douyin URL and later from remote page content, so the downloader context makes the issue more dangerous rather than less.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
def run_check(cmd: str, timeout: int = 30) -> str:
    """Run a shell command and raise on failure."""
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
    if r.returncode != 0:
        raise RuntimeError(f"Command failed: {cmd}\n{r.stderr}")
    return r.stdout.strip()
Confidence
99% confidence
Finding
This instance is especially risky because run_check is used with agent-browser open '{url}', where url is directly derived from a user argument and only lightly normalized. A maliciously crafted URL containing quotes or shell operators can escape the quoting and abuse the shell to run arbitrary commands under the agent's privileges.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
f"-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' "
        f"'{video_src}'"
    )
    r = subprocess.run(dl_cmd, shell=True, capture_output=True, text=True, timeout=300)
    if r.returncode != 0:
        print(f"❌ Download failed: {r.stderr}", file=sys.stderr)
        sys.exit(1)
Confidence
99% confidence
Finding
The curl invocation abuses a shell with externally influenced parameters from both local user input and remote website-controlled data. Because the skill intentionally visits untrusted web content to extract media URLs, an attacker controlling a page, redirect, or title-derived filename can potentially achieve command execution or overwrite arbitrary files, making the downloader context materially dangerous.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill explicitly instructs use of both network access (`agent-browser`, `curl`) and shell execution, but the manifest does not declare any tool restrictions or permissions. That creates a real security gap because an agent may grant broader-than-necessary capabilities to a skill that fetches attacker-controlled URLs and executes external commands, increasing the blast radius if the skill is modified, misused, or chained with prompt-injection content from remote pages.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd: str, timeout: int = 30) -> str:
    """Run a shell command and return stdout."""
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
    return r.stdout.strip()
Confidence
98% confidence
Finding
The helper executes arbitrary shell strings with shell=True, and this function is later used with user-influenced values such as URLs and browser-evaluated content. That creates a command injection path where crafted input containing shell metacharacters or embedded quotes can break out of the intended command and execute arbitrary OS commands.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_check(cmd: str, timeout: int = 30) -> str:
    """Run a shell command and raise on failure."""
    r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
    if r.returncode != 0:
        raise RuntimeError(f"Command failed: {cmd}\n{r.stderr}")
    return r.stdout.strip()
Confidence
98% confidence
Finding
This wrapper repeats the same unsafe shell execution pattern and is directly used to open a browser on a user-supplied URL via string interpolation. Because shell=True interprets the full string, an attacker can supply a malicious URL that injects additional commands, leading to arbitrary code execution on the host running the skill.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
f"-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' "
        f"'{video_src}'"
    )
    r = subprocess.run(dl_cmd, shell=True, capture_output=True, text=True, timeout=300)
    if r.returncode != 0:
        print(f"❌ Download failed: {r.stderr}", file=sys.stderr)
        sys.exit(1)
Confidence
97% confidence
Finding
The download command is built as a shell string containing output_path and video_src, both of which can be influenced by external input: filename/output-dir come from arguments, and video_src is extracted from an untrusted remote page. If either contains a single quote or shell metacharacters, the quoted curl command can be broken and arbitrary commands executed.

Static analysis

No suspicious patterns detected.