Back to skill

Security audit

JoeVault

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local cleanup skill, but its quarantine script can move data outside the intended archive path if given an unsafe label.

Review and harden scripts/quarantine_path.py before installing or using this broadly. Only run it with explicit source paths you have audited, use simple label names with no slashes or '..', avoid moving large or recently active workspaces, and keep backups because quarantine moves are persistent.

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
scripts/quarantine_path.py:27
Finding

Archive Path Traversal Through Unvalidated Label

Content
View full analysis

Vulnerability Details

File Location: scripts/quarantine_path.py, lines 27-43
Vulnerability Type: Path traversal and archive-boundary escape
Risk Level: Medium

Vulnerable Code

python
ap.add_argument('--label', required=True, help='Target folder/file name inside the archive root')
ap.add_argument('--note')
args = ap.parse_args()

source = Path(args.source)
archive_root = Path(args.archive_root)
target = archive_root / args.label
manifest = archive_root / 'MANIFEST.md'

if not source.exists() and not source.is_symlink():
    raise SystemExit(f'Source does not exist: {source}')
if target.exists():
    raise SystemExit(f'Target already exists: {target}')

archive_root.mkdir(parents=True, exist_ok=True)
shutil.move(str(source), str(target))
append_manifest(manifest, source, target, args.note)
print(f'Moved {source} -> {target}')
print(f'Updated manifest: {manifest}')

Technical Analysis

The script states that --label is a target name inside the archive root, but it accepts the argument without validating its path semantics. The expression archive_root / args.label does not enforce containment:

  • An absolute label causes pathlib to discard archive_root.
  • A relative label containing .. can resolve outside archive_root.
  • Labels containing path separators can create unintended nested destinations.

The script only checks whether the resulting target already exists. It does not normalize the destination and verify that it remains beneath the resolved archive root. Consequently, a caller who controls command-line arguments can make the quarantine operation move a valid source to an arbitrary nonexistent destination permitted by the process's filesystem privileges.

Attack Path

  1. The attacker or unsafe automation selects an existing source path that the process can move.

  2. It invokes the script with an escaping label, for example:

    bash
    python3 scripts/quarantine_path.py \
      --source /path/to/source \
    

...[truncated 1244 chars]

Remediation
View remediation

Remediation Suggestions

Treat --label strictly as one filesystem entry name rather than a path:

  1. Reject empty labels, absolute paths, . and ...
  2. Reject labels containing /, \, or multiple path components.
  3. Require Path(args.label).name == args.label.
  4. Resolve the archive root and proposed target, then explicitly enforce containment before moving.
  5. Perform the containment check before creating directories or modifying the source.
  6. Add tests for absolute labels, traversal sequences, mixed separators, and valid ordinary labels.
  7. Consider adding --dry-run and explicit confirmation options for destructive moves.

Example hardening:

python
label = Path(args.label)
if (
    not args.label
    or label.is_absolute()
    or args.label in {'.', '..'}
    or label.name != args.label
    or '/' in args.label
    or '\\' in args.label
):
    raise SystemExit('Label must be a single file or directory name')

archive_root = Path(args.archive_root).resolve()
target = (archive_root / args.label).resolve(strict=False)

if not target.is_relative_to(archive_root):
    raise SystemExit('Target must remain inside the archive root')
if target == archive_root:
    raise SystemExit('Target cannot be the archive root')
if target.exists() or target.is_symlink():
    raise SystemExit(f'Target already exists: {target}')

The source may also be normalized and checked against policy-defined protected paths before shutil.move() is called.

Vulnerability Patterns
  • 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
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (1)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
85% confidence
Finding

The skill instructs the operator to run local scripts that read from and move filesystem paths, but it does not declare any explicit tool scope such as allowed-tools or permissions. That mismatch weakens reviewability and can enable broader-than-expected file access or modification if the agent runtime infers capabilities implicitly, especially because the skill deals with home directories, workspaces, and archival moves.

Content

No source excerpt is available for this finding.