Back to skill

Security audit

video-shot-demos

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its video-demo purpose, but its helper scripts handle paths unsafely enough that crafted inputs could run local commands or write outside the chosen output folder.

Review before installing. Use this only with trusted plan.json files, trusted project directories, and trusted BROWSER_PATH values. Prefer SHOT_ENGINE=cdp until shot.js is fixed to avoid shell command construction, and keep outputs in a disposable directory. Be aware that rendered pages may load Google Fonts, and ignore or remove the hard-coded local Windows archive reference unless you explicitly want to inspect that path.

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/shot.js:13
Finding
Shell Command Injection in the Screenshot Utility<![CDATA[ ## Vulnerability Details **File Location**: `scripts/shot.js:13, 26-30, 87` **Vulnerability Type**: OS command injection through shell command construction **Risk Level**: High ### Vulnerable Code ```js const fs=require('fs'),path=require('path'),{execSync}=require('child_process'); const[, ,file,targetMs,out,fx,fy,sc]=process.argv; function findBrowser(){ const cands=[]; if(process.env.BROWSER_PATH)cands.push(process.env.BROWSER_PATH); // ... } execSync( '"'+browser+ '" --headless=new --disable-gpu --hide-scrollbars --user-data-dir="'+profile+ '" --no-first-run --no-default-browser-check --screenshot="'+path.resolve(out)+ '" --window-size=1920,1080 --virtual-time-budget='+budget+ ' "file:///'+tmp.replace(/\\/g,'/')+'"', {stdio:'pipe',timeout:120000} ); ``` ### Technical Analysis The utility passes a dynamically constructed string to `child_process.execSync`. Unlike an argument-array API, `execSync` interprets the supplied string through the operating-system shell. The following values are inserted into the command without shell-safe escaping: - The user-controlled screenshot output argument, `out` - The environment-controlled browser path, `BROWSER_PATH` - Other dynamically constructed filesystem paths Wrapping a value in double quotes is not sufficient shell escaping. An attacker-controlled value containing a quote can terminate the intended quoted argument, after which shell metacharacters can introduce an additional command. Resolving the output with `path.resolve()` normalizes the path but does not remove quotes, command separators, command substitutions, or other shell syntax. The CDP fallback in `scripts/cdp-shot.js` does not have this defect because it invokes `spawn(browser, args, ...)` with an argument array. The vulnerable CLI path is attempted by default unless `SHOT_ENGINE=cdp` is set. ### Attack Path 1. An attacker gains control over the output filename supplied to `scripts/shot.js`, or controls the `BROWS ...[truncated 1323 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace `execSync` with an argument-array API that does not invoke a shell: ```js const { spawnSync } = require('child_process'); const args = [ '--headless=new', '--disable-gpu', '--hide-scrollbars', `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check', `--screenshot=${path.resolve(out)}`, '--window-size=1920,1080', `--virtual-time-budget=${budget}`, `file:///${tmp.replace(/\\/g, '/')}` ]; const result = spawnSync(browser, args, { stdio: 'pipe', timeout: 120000, shell: false }); if (result.error) throw result.error; if (result.status !== 0) { throw new Error(`Browser exited with status ${result.status}`); } ``` 2. Alternatively, use `execFileSync(browser, args, options)`, which also separates the executable from its arguments. 3. Validate `BROWSER_PATH` before execution: - Require an absolute path. - Verify that it exists and is a regular executable file. - Reject NUL bytes and control characters. - Where practical, restrict it to an administrator-approved browser allowlist. 4. Validate output paths: - Require a `.png` extension. - Resolve the path under a designated screenshot directory. - Verify with `path.relative()` that the resolved output remains inside that directory. 5. Consider making the existing CDP implementation the only execution path, since it already uses `spawn` with `shell: false`. 6. Add regression tests using arguments containing quotes, spaces, command separators, and command-substitution characters. The tests should verify that such values are rejected or passed as literal arguments and never interpreted by a shell. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/init.js:39
Finding
Output Directory Escape Through an Unvalidated Project Name<![CDATA[ ## Vulnerability Details **File Location**: `scripts/init.js:39-50, 54-75, 89-94` **Vulnerability Type**: Path traversal and unrestricted filesystem write **Risk Level**: Medium ### Vulnerable Code ```js let plan; try { plan = JSON.parse(fs.readFileSync(path.resolve(planPath), 'utf8')); } catch (e) { die('plan.json read/parse failed: ' + e.message); } if (!plan.project || !Array.isArray(plan.assign) || !plan.assign.length) { die('plan.json is missing project or assign'); } const root = opt('root'); const outDir = path.resolve(root, plan.project); if (fs.existsSync(outDir)) { const exist = fs.readdirSync(outDir); if (exist.some(f => /^shot-.*\.html$/.test(f)) && !FORCE) { die(`Output directory already contains shot pages: ${outDir}`); } } fs.mkdirSync(outDir, { recursive: true }); const ananSrc = path.join( SKILL, 'assets', 'examples', ananSource, 'anan - emotion rename' ); const sherySrc = path.join(SKILL, 'assets', 'shery - emotion rename'); const check = (label, dir, min, expect) => { // ... const n = copyDir(dir, path.join(outDir, path.basename(dir))); // ... }; for (const a of plan.assign) { const fname = `shot-${a.id}_${safeName(a.title)}.html`; const fpath = path.join(outDir, fname); if (fs.existsSync(fpath)) { dupShots.push(fname); continue; } // HTML generation omitted fs.writeFileSync(fpath, html, 'utf8'); } ``` ### Technical Analysis `plan.project` is loaded from a plan JSON file and passed directly to: ```js path.resolve(root, plan.project) ``` The code assumes that the result will remain below the user-supplied `--root`, but it does not enforce that assumption. If `plan.project` is an absolute path, the intended root can be discarded. If it contains parent-directory components such as `..`, the resolved path can move above the intended root. The resulting `outDir` is subsequently used for: - Recursive directory creation - Copying bundled character assets - Copying project ico ...[truncated 1728 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict `plan.project` to a single directory name rather than accepting a path: ```js function validateProjectName(value) { const name = String(value); if ( !name || path.isAbsolute(name) || name === '.' || name === '..' || name.includes('/') || name.includes('\\') || /[\0-\x1f\x7f]/.test(name) ) { die('Invalid project name'); } return name; } ``` 2. Enforce containment after resolving the path: ```js const rootDir = path.resolve(root); const projectName = validateProjectName(plan.project); const outDir = path.resolve(rootDir, projectName); const relative = path.relative(rootDir, outDir); if ( relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) ) { die('The output directory must remain inside --root'); } ``` 3. Validate all plan-derived filename components, including `a.id`, before using them in `path.join`. Use a strict identifier expression such as: ```js if (!/^\d+-\d+[a-z]?$/.test(String(a.id))) { die(`Invalid shot identifier: ${a.id}`); } ``` 4. Apply a reusable safe-join helper to every generated output path and verify containment after resolution. 5. Do not copy files into a pre-existing unrelated directory by default. Require the project directory to be newly created, or require an explicit, narrowly scoped overwrite option. 6. Use exclusive file creation where overwriting is not required: ```js fs.writeFileSync(fpath, html, { encoding: 'utf8', flag: 'wx' }); ``` 7. Add tests covering: - Absolute project paths - `../` and nested traversal - Windows drive and UNC paths - Mixed path separators - Symlinked output directories - Invalid shot identifiers containing separators or traversal components ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Description-Behavior Mismatch

Low
Confidence
90% confidence
Finding
The page makes network requests to Google Fonts, which breaks the expectation of a self-contained local demo and leaks viewer metadata such as IP address, user agent, and access timing to a third party. In an offline or privacy-sensitive workflow, this creates unnecessary supply-chain and tracking exposure even if the content itself is not executing untrusted code.

Context-Inappropriate Capability

Low
Confidence
80% confidence
Finding
The demo fetches Google Fonts from external domains, which creates an outbound network dependency and leaks viewer metadata such as IP address, user agent, and access timing to a third party. In a skill described as a local storyboard/demo generation tool, this weakens privacy and offline reproducibility, even though it is not a direct code-execution issue.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
This bundled HTML example is not a neutral animation asset: it explicitly stages attack-surface mapping, bypassing blacklists, trying alternate tags, and re-testing offensive findings. In a skill whose stated purpose is video shot/demo generation, embedding celebratory offensive-security walkthrough content can normalize misuse, encourage harmful operator behavior, and make the package functionally dual-use beyond its declared scope.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The example goes beyond abstract security storytelling and specifically praises adaptation techniques like swapping blocked payloads, using uncommon tags, and bypassing defenses. Because this is shipped as an example asset inside a non-security media-generation skill, it materially lowers the barrier to misuse by turning the sample into tacit guidance for evasion behavior.

Context-Inappropriate Capability

Low
Confidence
89% confidence
Finding
The reference file exposes and directs use of a specific local Windows path (`C:\Users\UncleC\Desktop\...`) outside the skill’s packaged examples. That creates environment-coupled behavior and can leak developer workstation details, while encouraging access to files not scoped to the skill itself.

Intent-Code Divergence

Low
Confidence
82% confidence
Finding
The document first asserts all style cards are verifiable within the local example set, then relies on an external archive outside those references for additional '实战风格'. This inconsistency can mislead an agent into searching beyond the intended workspace, weakening boundary assumptions and increasing the chance of unintended local file access.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The HTML performs third-party font fetches without any notice to the user, so users may believe the demo is fully local while it silently contacts Google infrastructure. This is primarily a privacy and transparency issue, and it becomes more relevant because the skill is described as a local generation/viewing toolchain.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/batch-shot.js:45

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/cdp-shot.js:119

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/shot.js:87