Back to skill

Security audit

sn-motion-html

Security checks for vulnerabilities and agentic risk

Overview

The skill’s main purpose is coherent, but its video pipeline can send API credentials and local project content to an unvalidated endpoint and lacks strong path confinement for manifest-controlled files.

Review before installing or running on projects you did not create. Only use a trusted .env and a verified Ark endpoint, inspect video-manifest.json for absolute paths or ../ traversal, and avoid running the generation pipeline on untrusted project folders until path and endpoint validation are added.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/seedance_pipeline.py:47
Finding
Bearer Credential Disclosure Through an Unrestricted API Base URL<![CDATA[ ## Vulnerability Details **File Location**: `scripts/seedance_pipeline.py:47-62` and `scripts/seedance_pipeline.py:286-289` **Vulnerability Type**: Unvalidated credential-bearing network destination **Risk Level**: High ### Vulnerable Code ```python class ArkClient: def __init__(self, key: str, base_url: str) -> None: self.key = key self.base_url = base_url.rstrip("/") def request(self, method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]: body = None if payload is None else json.dumps(payload).encode("utf-8") request = Request( f"{self.base_url}{path}", data=body, method=method, headers={ "Authorization": f"Bearer {self.key}", "Content-Type": "application/json", "User-Agent": "sn-motion-html/1.0", }, ) ``` ```python key = os.getenv("ARK_API_KEY") or os.getenv("VOLCENGINE_API_KEY") if not key: raise SystemExit("ARK_API_KEY is not set in the project-local .env file") client = ArkClient(key, os.getenv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")) ``` ### Technical Analysis The API destination is taken directly from the `ARK_BASE_URL` environment variable. The value is not restricted to HTTPS, compared against an allowlist, or otherwise verified as an official Volcengine Ark endpoint. The client subsequently attaches the user's Ark bearer credential to every request sent to this destination. Generation requests also contain the text prompt and Base64-encoded conditioning images. Consequently, a modified project-local `.env` file can redirect credential-bearing requests to an arbitrary server. Supporting a configurable provider endpoint can be legitimate, but automatically transmitting a production credential to any configured origin exceeds the minimum privilege needed for the declared Seedance integration. The default endpoint is legitimate; the ...[truncated 1463 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict the default client to an explicit allowlist of official Ark hosts: ```python from urllib.parse import urlsplit OFFICIAL_ARK_HOSTS = {"ark.cn-beijing.volces.com"} def validate_base_url(value: str) -> str: parsed = urlsplit(value) if parsed.scheme != "https": raise ValueError("ARK_BASE_URL must use HTTPS") if parsed.hostname not in OFFICIAL_ARK_HOSTS: raise ValueError("ARK_BASE_URL is not an approved Ark endpoint") if parsed.username or parsed.password or parsed.fragment: raise ValueError("ARK_BASE_URL contains unsupported URL components") return value.rstrip("/") ``` 2. Do not attach an Ark credential to an untrusted origin. If custom enterprise endpoints are required, maintain a separately configured allowlist rather than accepting arbitrary origins. 3. Require explicit user confirmation before the first request to any non-default endpoint, clearly displaying the scheme and hostname without displaying the credential. 4. Reject plaintext HTTP endpoints. 5. Avoid loading provider destination settings from untrusted project content when a trusted installation-level configuration is available. 6. Add tests proving that HTTP URLs, user-info URLs, lookalike domains, redirect-based destination changes, and unapproved hosts are rejected. 7. Consider disabling automatic redirects for credential-bearing requests or verify that redirects remain on the approved HTTPS origin before resending authorization headers. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/seedance_pipeline.py:159
Finding
Manifest Path Traversal Enables Local Data Exfiltration and Arbitrary File Writes<![CDATA[ ## Vulnerability Details **File Location**: `scripts/seedance_pipeline.py:37-41`, `scripts/seedance_pipeline.py:159-187`, and `scripts/seedance_pipeline.py:250-260` **Vulnerability Type**: Unvalidated manifest-controlled filesystem paths **Risk Level**: High ### Vulnerable Code ```python def data_uri(path: Path) -> str: mime = mimetypes.guess_type(path.name)[0] or "image/png" encoded = base64.b64encode(path.read_bytes()).decode("ascii") return f"data:{mime};base64,{encoded}" ``` ```python def generate_clip( root: Path, client: ArkClient, manifest: dict[str, Any], kind: str, item: dict[str, Any], first: Path, last: Path | None, args: argparse.Namespace, ) -> None: clip_id = item["id"] output = root / item["output"] if output.exists() and output.stat().st_size > 0 and not args.force: print(f"skip {kind} {clip_id} (already exists)") return if not first.exists() or (last is not None and not last.exists()): raise FileNotFoundError(f"Missing conditioning frame for {clip_id}") prompt_path = root / item["prompt"] prompt = prompt_path.read_text(encoding="utf-8").strip() style_motion_path = root / "prompts" / "style-motion.txt" if style_motion_path.exists(): style_motion = style_motion_path.read_text(encoding="utf-8").strip() if style_motion: prompt = f"{style_motion}\n\n{prompt}" raw = root / "tmp" / "seedance" / "raw" / kind / f"{clip_id}.mp4" tasks = root / "tmp" / "seedance" / "tasks" tasks.mkdir(parents=True, exist_ok=True) ``` ```python def run_dives(root: Path, client: ArkClient, manifest: dict[str, Any], args: argparse.Namespace) -> None: items = manifest.get("dives", [])[: args.limit or None] frames = root / "assets" / "video" / "frames" def run_one(item: dict[str, Any]) -> None: generate_clip(root, client, manifest, "dives", item, root / item["still"], None, args) output = root / ...[truncated 3659 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve and validate every manifest-derived path before reading or writing: ```python def confined_path(root: Path, value: str, allowed_dir: Path) -> Path: candidate = Path(value) if candidate.is_absolute(): raise ValueError("Absolute paths are not allowed") base = allowed_dir.resolve() resolved = (root / candidate).resolve(strict=False) if not resolved.is_relative_to(base): raise ValueError(f"Path escapes allowed directory: {value}") return resolved ``` 2. Apply dedicated directory restrictions: - Prompts: `root/prompts` - Conditioning images: `root/assets/images` - Final videos: `root/assets/video` - Raw files and metadata: `root/tmp/seedance` 3. Reject `..` components and absolute paths at manifest validation time, even if later resolution would remain inside an allowed directory. 4. Reject symlinks for sensitive inputs and output parents, or resolve them and verify that the final target remains within the approved directory. 5. Validate identifiers such as `clip_id`, `from`, and `to` against a restrictive pattern: ```python import re if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", clip_id): raise ValueError("Invalid clip identifier") ``` 6. Validate input type and size before encoding: - Permit only expected image extensions. - Decode images with a trusted image library to confirm their format. - Impose a maximum file size. - Avoid treating an unknown file as `image/png`. 7. Create output files using safe, application-generated names rather than directly using manifest values where possible. 8. Validate the complete manifest against a strict schema before any network or filesystem operation. 9. Add regression tests for absolute paths, traversal sequences, nested traversal, symbolic links, malicious identifiers, and paths escaping through output parent directories. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (16)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The description presents a broad end-to-end storytelling system for generating continuous-shot HTML story experiences. This code chunk does something much narrower and different: it post-processes already-generated video outputs into a labeled contact sheet for review. While such a tool could support QA in a larger media pipeline, the declared purpose does not mention contact-sheet generation or frame extraction, and the code does not itself perform the core declared functions like story building, HTML generation, styling, or clip creation. Therefore the code chunk’s actual behavior is materially different from the declared primary purpose.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared description is about creating motion-driven HTML story experiences and associated content-production workflows. The supplied code does not implement story generation, media creation, styling, normalization, research, or QA logic. Instead, it provides a standalone local file server with a safety feature to refuse hidden files. While local serving could be a minor supporting utility in a broader storytelling toolchain, this chunk’s primary purpose is materially different from the declared purpose and introduces undeclared hosting behavior. Therefore this is a clear description-behavior mismatch.

Ae1

High
Category
analysis-evasion
Content
python scripts/seedance_pipeline.py . plan
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python scripts/seedance_pipeline.py . plan
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
def load_dotenv(root: Path) -> None:
    path = root / ".env"
    if not path.exists():
        return
    for raw in path.read_text(encoding="utf-8").splitlines():
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def load_dotenv(root: Path) -> None:
    path = root / ".env"
    if not path.exists():
        return
    for raw in path.read_text(encoding="utf-8").splitlines():
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
raise SystemExit("ffmpeg is required")
    key = os.getenv("ARK_API_KEY") or os.getenv("VOLCENGINE_API_KEY")
    if not key:
        raise SystemExit("ARK_API_KEY is not set in the project-local .env file")
    client = ArkClient(key, os.getenv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"))
    if args.phase in {"dives", "all"}:
        run_dives(root, client, manifest, args)
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill instructs use of shell commands, local file operations, networking, environment-based secrets, and a local web server, but it does not declare any explicit tool scope or allowed-tools boundary. In an agent environment, that omission can lead to over-broad authority, making it easier for the skill to access credentials, modify unexpected files, or perform network actions beyond what a user intended.

Vague Triggers

Medium
Confidence
89% confidence
Finding
The manifest's default prompt says 'Use $sn-motion-html to build...' but does not define when this skill should activate, what exact trigger phrases are supported, or any exclusion conditions. In a manifest file, this kind of sparse invocation guidance can be ambiguous and may lead to unintended triggering because the scope and boundaries of the skill are not specified.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
frames: list[tuple[str, Path]] = []
    for index, (kind, item) in enumerate(records):
        frame = frame_dir / f"{index:03d}-{kind}-{item['id']}.jpg"
        subprocess.run([
            "ffmpeg", "-v", "error", "-y", "-ss", str(duration / 2),
            "-i", str(root / item["output"]), "-frames:v", "1", str(frame),
        ], check=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
def run_ffmpeg(args: list[str]) -> None:
    subprocess.run(["ffmpeg", "-v", "error", "-y", *args], check=True)


def normalize_video(source: Path, target: Path, manifest: dict[str, Any]) -> None:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script reads prompt text and local conditioning images, converts those images into data URIs, and sends both to a remote generation API. In this skill context, story prompts and images may contain proprietary or sensitive creative material, so silent transmission to a third party creates a real privacy and data-governance risk even if it is functionally intended.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The client defaults to a hard-coded China-region API endpoint when ARK_BASE_URL is not set, causing prompts, images, and generated media workflow metadata to be transmitted to a specific foreign-region service by default. That creates jurisdiction, compliance, and user-expectation risks because users are not forced to make an informed endpoint choice before data is sent.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
"-show_entries", "stream=codec_name,width,height,r_frame_rate,nb_frames:format=duration",
            "-of", "json", str(path),
        ]
        data = json.loads(subprocess.check_output(command, text=True))
        stream = data["streams"][0]
        duration = float(data["format"]["duration"])
        okay = (
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
Confidence
78% confidence
Finding
The manifest presents the skill description in Chinese while the default prompt is fixed in English, but there is no indication that the user can choose their preferred language. This may create a language-policy concern because the skill appears to impose locale behavior implicitly rather than documenting or offering language opt-in.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
The document declares `lang="en"`, which hard-codes English as the page language. Under the policy, forcing a specific language without user choice or clear region-specific justification is a natural-language policy concern.

Static analysis

No suspicious patterns detected.