Skip to content

MCP Security and Risk management #91

@Dimwiddle

Description

@Dimwiddle

Security — v0.3.0 Scope

Attack surface overview

The 3+1 MCP design has a smaller attack surface than a full-tool MCP server (one write operation instead of nine), but three vectors require mitigation before release.

Attack surfaces:
├── Skill file injection       ← Agent reads tampered instructions from disk
├── Init write safety          ← Symlinks or overwrites during project setup
├── CLI argument injection     ← Shell metacharacters in agent-supplied values
├── Contract spoofing          ← False guarantees from forked/modified server (future)
├── Status information leakage ← Sensitive feature names in resource responses (low risk)
└── Resource DoS               ← Repeated parsing of large feature sets (low risk)

Risk priority matrix

Risk Severity Likelihood v0.3.0 action
Skill file injection High Medium Mitigate
Init path traversal / symlink High Low Mitigate
CLI argument injection High Low Verify & tighten
Contract spoofing Medium Low Document; defer signed contracts
Status information leakage Low Medium Document guidance only
Resource DoS Low Low Cache if needed; defer

1. Skill file integrity verification

Important This has been implemented in previous issue

Can skip from scope of this issue. Verify the below works as expected.


2. Init write safety

Risk: specleft_init is the only MCP tool and the only write operation exposed through the protocol. Path traversal or symlink attacks could write files to unintended locations.

v0.3.0 deliverables:

A. No user-supplied paths in MCP tool schema

The init tool schema accepts only example, blank, and dry_run booleans. All output paths are hardcoded relative to the current working directory. No dir or output parameter is exposed through MCP. This is already the case in the current design — explicitly preserve this constraint.

B. Symlink detection before all writes

Before creating any directory or file, check that the target path is not a symlink:

import os

def safe_write_path(path: str) -> str:
    """Validate path is safe to write to. Raises SecurityError if not."""
    resolved = os.path.realpath(path)
    cwd = os.path.realpath(os.getcwd())
    
    # Must resolve within project directory
    if not resolved.startswith(cwd):
        raise SecurityError(f"Path resolves outside project: {path} -> {resolved}")
    
    # Must not be a symlink
    if os.path.islink(path):
        raise SecurityError(f"Refusing to write through symlink: {path}")
    
    # Check parent directories for symlinks
    parent = os.path.dirname(path)
    while parent and parent != cwd:
        if os.path.islink(parent):
            raise SecurityError(f"Parent directory is a symlink: {parent}")
        parent = os.path.dirname(parent)
    
    return resolved

C. Warn on existing modified files, don't overwrite

If .specleft/SKILL.md exists and its checksum doesn't match the expected template for the installed version:

{
  "success": true,
  "warnings": [
    "Existing SKILL.md has been modified from template (checksum mismatch). Use --force to regenerate."
  ],
  "skill_file_regenerated": false
}

Only --force overwrites a modified skill file. Unmodified skill files (checksum matches previous version's template) are silently upgraded.


3. CLI argument validation

Risk: Agents construct shell commands from the skill file, potentially passing unsanitised user input as CLI arguments. Shell metacharacters in feature IDs or titles could enable command injection.

v0.3.0 deliverables:

A. Strict validation at the Click parameter level

All ID parameters (--id, --feature, --story) validated against:

import re
import click

def validate_id(ctx, param, value):
    """Enforce kebab-case alphanumeric IDs. Reject shell metacharacters."""
    if value is None:
        return value
    if not re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', value):
        raise click.BadParameter(
            f"Must be kebab-case alphanumeric (got: {value!r}). "
            "Characters allowed: a-z, 0-9, hyphens."
        )
    return value

All text parameters (--title, --description, --name) validated against:

SHELL_METACHARACTERS = set('$`|;&><(){}[]!\\\'\"')

def validate_text(ctx, param, value):
    """Reject shell metacharacters in text inputs."""
    if value is None:
        return value
    dangerous = SHELL_METACHARACTERS.intersection(value)
    if dangerous:
        raise click.BadParameter(
            f"Contains disallowed characters: {dangerous}. "
            "Avoid shell metacharacters in text inputs."
        )
    return value

B. Audit all existing Click parameters

Review every CLI command for parameters that accept freeform text. Ensure validation is applied at the parameter decorator level, not deferred to business logic. This prevents any code path from receiving unsanitised input.

C. Skill file safety note

Include in the generated skill file:

## Safety
- All --id values must be kebab-case alphanumeric (a-z, 0-9, hyphens)
- All text inputs reject shell metacharacters ($, `, |, ;, &, etc.)
- Never pass unsanitised user input directly as CLI arguments
- All commands are single invocations — no pipes, chaining, or redirects
- Exit codes: 0=success, 1=error, 2=cancelled
- All commands deterministic and safe to retry

4. Contract payload updates

The contract resource payload expands to include security guarantees:

{
  "contract_version": "1.1",
  "specleft_version": "0.3.0",
  "guarantees": {
    "dry_run_never_writes": true,
    "no_writes_without_confirmation": true,
    "existing_files_never_overwritten": true,
    "skeletons_skipped_by_default": true,
    "skipped_never_fail": true,
    "deterministic_for_same_inputs": true,
    "safe_for_retries": true,
    "exit_codes": { "success": 0, "error": 1, "cancelled": 2 },
    "skill_file_integrity_check": true,
    "skill_file_commands_are_simple": true,
    "cli_rejects_shell_metacharacters": true,
    "init_refuses_symlinks": true,
    "no_network_access": true,
    "no_telemetry": true
  }
}

Target size: ~100 tokens (up from ~80). Marginal increase for significant trust signal improvement.


5. Deferred security work (post v0.3.0)

Item Description Target
Signed contracts Cryptographically sign contract payload during build. Agents verify against SpecLeft public key. Enforcement tier
Signed skill files Embed signature in skill file header. Verification without separate checksum file. v0.4.0
Status redaction --redact flag on status output for teams with strict information controls. On demand
Resource caching Cache status resource responses with short TTL for large projects. On demand
Feature name guidance Document in guide resource: avoid sensitive information in feature/scenario names. v0.3.0 docs

Competitive security positioning

Most MCP servers ship with zero security story. SpecLeft v0.3.0 will have:

  • Agent Contract with machine-verifiable guarantees (unique in the spec-driven tooling space)
  • Skill file integrity verification with checksums (no competitor has instruction file verification)
  • Strict input validation rejecting shell metacharacters (defense-in-depth for agent-executed commands)
  • Symlink-safe writes with path traversal protection
  • No network access, no telemetry (verifiable by design — the MCP server is stdio-only)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions