Back to skill

Security audit

Video Color Grade

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherently built for video color grading, but untrusted look files can control FFmpeg filters and output filenames too broadly.

Install only if you are comfortable running media-processing scripts in your project. Use locally authored or trusted looks.json/grade-plan files, avoid shared or sensitive output directories, and consider running the workflow in a sandbox because crafted look specs may cause FFmpeg to read, write, overwrite, or consume resources in ways not limited to simple color grading.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/gradelib.py:72
Finding

Untrusted Grade Specifications Are Executed as FFmpeg Filtergraphs

Content
View full analysis

Vulnerability Details

File Location: scripts/gradelib.py:72-78; execution sinks in scripts/apply_grade.py:33-39, scripts/bake_lut.py:36-38,55-57, scripts/render_looks.py:38-41, and scripts/make_clips.py:22-27
Vulnerability Type: FFmpeg filtergraph injection
Risk Level: High

Vulnerable Code

python
def full_chain(spec, look):
    """A look's full ffmpeg filter = corrective base + ',' + creative layer."""
    base = (spec.get("base") or "").strip()
    layer = (look.get("chain") or "").strip()
    if look.get("prepend_base", True) and base:
        return base + ("," + layer if layer else "")
    return layer or "null"

Representative execution sink in scripts/apply_grade.py:

python
spec = gl.load_spec(args.looks); chain = gl.full_chain(spec, gl.get_look(spec, args.name))
gl.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", os.path.abspath(args.video),
        "-vf", chain, "-c:v", "libx264", "-crf", args.crf, "-preset", args.preset,
        "-pix_fmt", "yuv420p", "-c:a", "copy", "-movflags", "+faststart",
        os.path.abspath(args.out)])

Representative execution sink in scripts/make_clips.py:

python
gl.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-ss", str(ss), "-t", str(t),
        "-i", os.path.abspath(video), "-i", os.path.abspath(lp),
        "-filter_complex", f"[0:v]{chain}[g];[g][1:v]overlay=10:10[v]",
        "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "20", "-preset", "veryfast",
        "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart",
        os.path.abspath(out)])

Technical Analysis

The base and chain properties are read from a user-supplied JSON specification and concatenated without validating their syntax, length, filter names, options, or referenced resources. The resulting value is passed directly to FFmpeg through -vf or embedded in `-fi ...[truncated 2157 chars]

Remediation
View remediation

Remediation Suggestions

  1. Replace arbitrary filtergraph strings with a structured grade schema containing only explicitly supported operations and numeric parameters.
  2. Allowlist the point filters required by the documented workflow, such as colorbalance, eq, curves, and vibrance.
  3. For every allowed filter, validate option names, numeric types, finite values, and conservative ranges.
  4. Reject unknown filters, source filters, path-taking options, metadata-output filters, external resource references, and graph-control syntax not required by the workflow.
  5. Apply maximum lengths to all filter expressions and limits to the number of filters in a chain.
  6. Validate legacy specifications as strictly as canonical grade plans.
  7. Run FFmpeg in a restricted environment where practical, using a low-privilege account, filesystem sandbox, resource limits, and disabled network access.
  8. Add negative tests proving that path-taking filters, unexpected graph labels, extra inputs, and oversized expressions are rejected before FFmpeg starts.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/render_looks.py:38
Finding

Unvalidated Look Names Are Used to Construct Output Paths

Content
View full analysis

Vulnerability Details

File Location: scripts/render_looks.py:38-41; related output construction in scripts/make_clips.py:54-59
Vulnerability Type: Path traversal and output-path injection
Risk Level: Medium

Vulnerable Code

From scripts/render_looks.py:

python
panels = [("0_original", "ORIGINAL  ·  as shot", frame)]
for lk in spec["looks"]:
    out = os.path.join(args.out, f"look_{lk['name']}.png")
    gl.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", os.path.abspath(frame),
            "-vf", gl.full_chain(spec, lk), os.path.abspath(out)])
    panels.append((lk["name"], f"{lk['label']}  ·  {lk.get('desc','')}", out))

From scripts/make_clips.py:

python
for nm in names:
    lk = gl.get_look(spec, nm)
    render_clip(args.video, args.ss, args.t, gl.full_chain(spec, lk), lk["label"],
                os.path.join(args.out, f"clip_{nm}.mp4"), args.out)
    print(f"ok clip_{nm}.mp4")

Technical Analysis

normalize_spec() requires look names to be unique and non-empty, but it does not restrict path separators, traversal components, drive syntax, control characters, or filename length. Both rendering scripts embed look names directly into filesystem paths.

Filename prefixes such as look_ and clip_ do not constitute a security boundary. Crafted names containing separators and parent-directory components can produce nested or escaping paths when the necessary intermediate directories exist. The code does not resolve the final destination and verify that it remains under the intended output directory.

FFmpeg is invoked with -y, so it overwrites an existing destination without prompting.

Attack Path

  1. An attacker supplies a grade specification containing a crafted look name with path separators or traversal components.
  2. The user invokes render_looks.py, which processes every look, or selects the malicious name through `make_clips.py ...[truncated 1168 chars]
Remediation
View remediation

Remediation Suggestions

  1. Enforce a conservative identifier format for look names, such as ^[A-Za-z0-9_-]{1,64}$.
  2. Keep display labels separate from filesystem identifiers and never derive paths from labels.
  3. Resolve the output root and candidate destination before writing, then verify the candidate is a descendant of the output root.
  4. Reject absolute paths, path separators, .. components, drive prefixes, control characters, and reserved platform-specific names.
  5. Avoid unconditional overwrite for destinations derived from external data, or require an explicit overwrite option.
  6. Apply the same validation in normalize_spec() for both canonical and legacy specifications.
  7. Add cross-platform tests for POSIX separators, Windows separators, drive prefixes, traversal components, Unicode edge cases, and overlong names.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/bake_lut.py:34
Finding

Predictable Temporary Files Can Be Overwritten and Deleted

Content
View full analysis

Vulnerability Details

File Location: scripts/bake_lut.py:34-50; related temporary-file handling in scripts/make_clips.py:22,60-62
Vulnerability Type: Unsafe temporary-file handling
Risk Level: Medium

Vulnerable Code

From scripts/bake_lut.py:

python
work = os.path.dirname(out) or "."
N = args.size

# identity grid, red fastest: index = b*N*N + g*N + r
idx = np.arange(N**3)
r, g, b = idx % N, (idx // N) % N, idx // (N*N)
grid = (np.stack([r, g, b], 1).astype(np.float64) / (N-1) * 255).round().astype(np.uint8)
Image.fromarray(grid.reshape(N, N*N, 3), "RGB").save(os.path.join(work, "_grid_id.png"))
gl.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y",
        "-i", os.path.join(work, "_grid_id.png"), "-vf", chain,
        os.path.join(work, "_grid_out.png")])
o = (np.asarray(Image.open(os.path.join(work, "_grid_out.png")).convert("RGB"))
     .reshape(N**3, 3).astype(np.float64) / 255.0)

with open(out, "w") as f:
    f.write(f'TITLE "{args.name}"\nLUT_3D_SIZE {N}\n')
    f.write("DOMAIN_MIN 0.0 0.0 0.0\nDOMAIN_MAX 1.0 1.0 1.0\n")
    for i in range(N**3):
        f.write(f"{o[i,0]:.6f} {o[i,1]:.6f} {o[i,2]:.6f}\n")
for tmp in ("_grid_id.png", "_grid_out.png"):
    try: os.remove(os.path.join(work, tmp))
    except OSError: pass

Related code from scripts/make_clips.py:

python
lp = os.path.join(workdir, "_lbl.png"); label_png(label, lp)
python
try:
    os.remove(os.path.join(args.out, "_lbl.png"))
except OSError:
    pass

Technical Analysis

The scripts create temporary artifacts with fixed names inside caller-selected output directories. They do not check whether those paths already exist, create files exclusively, use a private temporary directory, or verify that an existing path is a regular file owned by the current operation.

In bake_lut.py, Pillow overwrites _grid_id.png, FFmpeg uses -y when writing `_grid_out ...[truncated 1686 chars]

Remediation
View remediation

Remediation Suggestions

  1. Create a private temporary directory with tempfile.TemporaryDirectory() for each invocation.
  2. Generate unpredictable temporary filenames using tempfile.NamedTemporaryFile() or UUID-based names.
  3. Use exclusive creation where possible and reject unexpected pre-existing paths.
  4. Keep temporary files outside the final output directory.
  5. Place cleanup in a try/finally block so failed FFmpeg or Pillow operations do not leave artifacts behind.
  6. Before deletion, verify that the target is the exact temporary path created by the current process.
  7. Avoid shared temporary names across concurrent look renders.
  8. Where supported, restrict temporary-directory permissions to the current user.
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 (4)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding

The skill instructs the agent to run shell commands and write durable project files, but it does not declare any explicit tool scope such as permissions or allowed-tools. That creates an authorization gap where a host agent may grant broader shell/file access than is actually needed, increasing the blast radius if the skill is misused, modified, or supplied with adversarial inputs such as crafted file paths or media artifacts.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/gradelib.py (reported line 22)May include surrounding context.

python
def run(cmd, cwd=None):
    r = subprocess.run([str(c) for c in cmd], cwd=cwd, capture_output=True, text=True)
    if r.returncode != 0:
        sys.stderr.write(r.stderr or "")
        raise SystemExit(f"\ncommand failed ({r.returncode}): {' '.join(map(str, cmd))}")

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
92% confidence
Finding

This code writes temporary files into the output directory and invokes ffmpeg with -y, which forces overwriting existing files, then deletes those temporary files afterward. Although these are expected implementation details, there is no docstring/comment/user-facing disclosure warning that the script will create, overwrite, and remove files in the target directory.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The script opens the destination path for writing and will overwrite any existing file at that location, but it does not prompt the user or explicitly warn about this behavior. For file-writing operations, some visible disclosure is expected unless the overwrite behavior is clearly documented.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.