Back to skill

Security audit

para-pkm

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate PARA knowledge-base purpose, but its helper scripts can overwrite, create, or delete files outside the chosen knowledge-base path when given crafted paths.

Install only if you trust yourself and your agents to run the scripts with simple KB-local paths. Avoid absolute paths or '..' components in arguments, be careful with --output, and consider fixing path validation before using it on important notes or repositories.

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/archive_project.py:22
Finding

Path Traversal Allows Reading and Deleting Files Outside the Knowledge Base

Content
View full analysis

Vulnerability Details

File Location: scripts/archive_project.py, lines 22-59
Vulnerability Type: Improper path validation and arbitrary file deletion
Risk Level: High

Vulnerable Code

python
# Resolve paths
if not project_file.is_absolute():
    project_file = kb_path / project_file

if not project_file.exists():
    print(f"❌ Project file not found: {project_file}")
    sys.exit(1)

# Determine source and destination
archives_path = kb_path / "archives"
if not archives_path.exists():
    archives_path.mkdir(parents=True)
    print(f"✅ Created archives/ directory")

# Create archived filename with date
date_str = datetime.now().strftime("%Y-%m-%d")
archived_name = f"{project_file.stem}-archived-{date_str}{project_file.suffix}"
dest_file = archives_path / archived_name

# Read original content
content = project_file.read_text()

# Add archive metadata at top
archive_note = f"""---
ARCHIVED: {datetime.now().strftime("%Y-%m-%d")}
Original location: {project_file.relative_to(kb_path)}
---

"""

archived_content = archive_note + content

# Write to archives
dest_file.write_text(archived_content)
print(f"✅ Archived to: {dest_file.relative_to(kb_path)}")

# Remove original
project_file.unlink()

Technical Analysis

The script joins the user-controlled project_file argument to kb_path without canonicalizing the resulting path or verifying that it remains within the knowledge base. A relative path containing .. components can therefore escape the expected projects/active/ directory and the knowledge-base root.

For example, if kb_path is /home/user/kb, the argument ../important.txt produces the lexical path /home/user/kb/../important.txt. The path passes the existence check, can be read, and remains lexically compatible with relative_to(kb_path) because it begins with the knowledge-base path before normalization. The script then copies the content into archives/ and permanently removes the original using unlink().

...[truncated 1183 chars]

Remediation
View remediation

Remediation Suggestions

  1. Resolve the knowledge-base root and candidate source before use:
    python
    kb_root = kb_path.resolve(strict=True)
    active_root = (kb_root / "projects" / "active").resolve(strict=True)
    source = (kb_root / project_file).resolve(strict=True)
    
  2. Reject absolute user-supplied project paths unless there is an explicit need to support them.
  3. Require the resolved source to be contained in projects/active/:
    python
    if not source.is_relative_to(active_root):
        raise ValueError("Project must be located under projects/active/")
    
  4. Require the source to be a regular file and define an explicit symlink policy. Reject symlinks when they are unnecessary.
  5. Resolve and validate the archive destination before writing.
  6. Refuse destination collisions instead of silently overwriting an archive created on the same date.
  7. Use an atomic move operation where possible rather than writing a copy and subsequently deleting the source.
  8. Add tests covering .., absolute paths, symlinks, nonexistent files, and destination collisions.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/generate_nav.py:101
Finding

Unrestricted Output Path Allows Arbitrary File Overwrite

Content
View full analysis

Vulnerability Details

File Location: scripts/generate_nav.py, lines 101-119
Vulnerability Type: Path traversal and arbitrary file overwrite
Risk Level: High

Vulnerable Code

python
parser.add_argument(
    "--output",
    type=str,
    default="AGENTS.md",
    help="Output filename (default: AGENTS.md)"
)

args = parser.parse_args()
kb_path = args.kb_path

if not kb_path.exists():
    print(f"❌ Path does not exist: {kb_path}")
    return 1

print(f"🔍 Scanning PARA structure: {kb_path}")

# Generate navigation
nav_content = generate_navigation(kb_path)

# Write to file
output_path = kb_path / args.output
output_path.write_text(nav_content)

Technical Analysis

The --output value is fully user-controlled and is appended to kb_path without validation. Python path joining does not guarantee containment:

  • A value such as ../../target.txt traverses outside the knowledge base.
  • If args.output is absolute, kb_path / args.output resolves to the absolute operand rather than a path beneath kb_path.

The subsequent write_text() call creates or truncates the selected file. No check ensures that the resolved output is inside the knowledge base, that it has an approved navigation filename, or that an existing unrelated file will not be overwritten.

Attack Path

  1. An attacker supplies or recommends a crafted invocation:
    bash
    python scripts/generate_nav.py \
      --kb-path /home/user/kb \
      --output ../../target.txt
    
  2. The script joins the traversal path to the knowledge-base path.
  3. write_text() opens the resulting external target in truncating write mode.
  4. Existing target content is replaced with generated navigation content.

An absolute output path can be used similarly:

bash
python scripts/generate_nav.py \
  --kb-path /home/user/kb \
  --output /home/user/target.txt

Impact Assessment

The script can overwrite any file writable by the invoking user. Potential targets include user ...[truncated 527 chars]

Remediation
View remediation

Remediation Suggestions

  1. Prefer an allowlist of supported output names, such as AGENTS.md and CLAUDE.md.
  2. Reject absolute paths and any output containing directory components if only a filename is required:
    python
    output_name = Path(args.output)
    if output_name.is_absolute() or output_name.name != args.output:
        raise ValueError("--output must be a filename within the knowledge-base root")
    
  3. Resolve and enforce containment before writing:
    python
    kb_root = kb_path.resolve(strict=True)
    output_path = (kb_root / output_name).resolve()
    if not output_path.is_relative_to(kb_root):
        raise ValueError("Output path escapes the knowledge-base root")
    
  4. Confirm that kb_path is a directory rather than checking only that it exists.
  5. Avoid following output symlinks, or explicitly reject symlink targets.
  6. Consider refusing to overwrite existing files unless the user supplies an explicit --force option.
  7. Write to a KB-local temporary file and atomically replace the approved destination.
  8. Add tests for absolute paths, traversal components, symlinks, existing targets, and non-directory KB paths.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/init_para_kb.py:17
Finding

Unvalidated Knowledge-Base Name Allows Directory Creation Outside the Selected Base Path

Content
View full analysis

Vulnerability Details

File Location: scripts/init_para_kb.py, lines 17-40 and 74-104
Vulnerability Type: Path traversal and unrestricted filesystem creation
Risk Level: Medium

Vulnerable Code

python
def create_para_structure(kb_name: str, base_path: Path) -> None:
    """Create PARA directory structure."""
    kb_path = base_path / kb_name

    if kb_path.exists():
        print(f"❌ Directory already exists: {kb_path}")
        sys.exit(1)

    # Create main PARA folders
    folders = [
        "projects/active",
        "projects/stories",
        "projects/stories/fragments",
        "areas",
        "resources",
        "archives",
    ]

    print(f"🚀 Creating PARA knowledge base: {kb_name}")
    print(f"   Location: {kb_path}\n")

    for folder in folders:
        (kb_path / folder).mkdir(parents=True, exist_ok=True)
        print(f"✅ Created {folder}/")

The selected path is later populated with files:

python
(kb_path / "README.md").write_text(readme_content)
print(f"✅ Created README.md")
python
(kb_path / "AGENTS.md").write_text(agents_content)
print(f"✅ Created AGENTS.md")

Technical Analysis

The positional kb_name argument is treated as a path rather than being validated as a single knowledge-base name. It may contain .. traversal components or be an absolute path. When the right-hand operand is absolute, pathlib discards the preceding base_path.

Consequently, the script can create the PARA directory tree, README.md, and AGENTS.md outside the directory selected by --path. The initial exists() check prevents creation when the final lexical target already exists, reducing overwrite risk, but it does not enforce containment or prevent creation in unintended writable locations.

Attack Path

  1. The user is induced to execute a crafted command:
    bash
    python scripts/init_para_kb.py ../unexpected-kb --path /home/user/knowledge
    
  2. The script forms `/home/user/knowledge/../ ...[truncated 804 chars]
Remediation
View remediation

Remediation Suggestions

  1. Treat kb_name strictly as a single name rather than a path:
    python
    candidate = Path(kb_name)
    if candidate.is_absolute() or candidate.name != kb_name or kb_name in {".", ".."}:
        raise ValueError("Knowledge-base name must be a single path component")
    
  2. Consider restricting names to a documented character set, such as letters, digits, spaces, underscores, and hyphens.
  3. Resolve the base directory and candidate target, then enforce containment:
    python
    base_root = base_path.resolve(strict=True)
    kb_path = (base_root / kb_name).resolve()
    if not kb_path.is_relative_to(base_root):
        raise ValueError("Knowledge-base path escapes the selected base directory")
    
  4. Verify that base_path exists and is a directory before creating content.
  5. Define and enforce a symlink policy for the base path and target parent directories.
  6. Create the root with exist_ok=False before adding child content to reduce race and collision risks.
  7. Add tests for absolute names, .. traversal, nested names, symlinked parents, and invalid base directories.
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
91% confidence
Finding

The skill instructs the agent to run local Python scripts that create, validate, archive, and generate navigation for a knowledge base, which implies file read/write capabilities. However, the manifest does not declare any explicit tool scope or permissions, creating an authorization gap where the skill's effective filesystem access is not transparently constrained or reviewable. In a PKM skill, this is especially relevant because operations naturally target user notes and directories, so undeclared file access increases the risk of unintended modification, overwriting, or broad traversal of local content.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.