Back to skill

Security audit

Project Router

Security checks for vulnerabilities and agentic risk

Overview

This project workflow skill is coherent in purpose, but it exposes unguarded tools that can run repository-defined commands and apply file writes on the user's machine.

Review this carefully before installing. Use it only in trusted repositories, inspect .project/targets.json and any .project/history/plans/*.json before invoking run or apply, and avoid running the MCP server with sensitive environment variables until command confirmation, path containment, and bundled-CLI verification are added.

Vulnerability Patterns
  • Tool Hijacking and SpoofingModifies or replaces tools so legitimate-looking calls execute attacker logic
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/project.js:135
Finding
Arbitrary File Write Through Unvalidated Plan Paths## Vulnerability Details **File Location**: `scripts/project.js:135-151` **Vulnerability Type**: Path traversal and arbitrary file write **Risk Level**: High ### Vulnerable Code ```javascript function applyPlan(root, planId) { const projDir = path.join(root, '.project'); const planPath = path.join(projDir, 'history', 'plans', `${planId}.json`); if (!exists(planPath)) die(`Plan not found: ${planId}`); const plan = readJson(planPath); const applied = []; for (const w of plan.writes || []) { const dst = path.join(root, w.path); if (w.kind === 'json') { writeJson(dst, w.content); } else { fs.mkdirSync(path.dirname(dst), { recursive: true }); fs.writeFileSync(dst, String(w.content)); } applied.push({ path: w.path, kind: w.kind }); } ``` ### Technical Analysis The application treats the contents of stored plan files as trusted and writes every `writes[].path` entry without validating its type, format, or final resolved location. `path.join(root, w.path)` normalizes path traversal sequences but does not guarantee that the resulting destination remains inside `root`. A path such as `../../target-file` can escape the project directory. The implementation also does not reject destinations that traverse through symbolic links pointing outside the project. The plan has no schema validation, integrity protection, signature, ownership check, or restriction requiring writes to remain inside `.project`. Anyone able to create or modify a plan file can therefore control both the destination and content of subsequent writes. ### Attack Path 1. An attacker obtains the ability to create or modify a JSON plan under `.project/history/plans/`. This can occur through a malicious or shared repository, compromised workspace content, or another process with repository write access. 2. The attacker adds a plan entry similar to: ```json { "writes": [ ...[truncated 1241 chars]
Remediation
## Remediation Suggestions 1. Validate every plan against a strict schema before applying it. Require a known `kind`, a relative string path, and bounded content size. 2. Resolve destinations and enforce containment: ```javascript const allowedRoot = path.resolve(root, '.project'); const dst = path.resolve(root, w.path); const relative = path.relative(allowedRoot, dst); if ( relative === '' || relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative) ) { throw new Error(`Plan path escapes allowed directory: ${w.path}`); } ``` 3. Restrict plan writes to an explicit allowlist of `.project` files and directories rather than the entire project root. 4. Reject absolute paths, null bytes, traversal components, device paths, and unsupported file types. 5. Detect symlink escapes by resolving the nearest existing parent with `fs.realpathSync` and verifying that it remains inside the canonical allowed root. 6. Open destination files using safe flags where appropriate, and avoid following symlinks when the platform supports such controls. 7. Protect saved plans against modification by recording a cryptographic digest or signature at creation and verifying it immediately before application. 8. Present the normalized list of destination files to the user and require confirmation before applying plans that modify sensitive locations.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/project.js:226
Finding
Repository-Controlled Shell Command Execution With Inherited Environment## Vulnerability Details **File Location**: `scripts/project.js:226-232` **Vulnerability Type**: Untrusted command execution **Risk Level**: High ### Vulnerable Code ```javascript const results = []; for (const cmd of commands) { const r = spawnSync(cmd, { cwd: root, shell: true, stdio: 'inherit', env: process.env, }); ``` ### Technical Analysis Target commands are read from the project-controlled `.project/targets.json` file and passed directly to `spawnSync` with `shell: true`. Consequently, each target entry is interpreted as a shell program rather than as a fixed executable with separately encoded arguments. The configuration file is part of the repository and may be supplied or modified by an untrusted project author. Running a target therefore provides that author with arbitrary command execution under the MCP server or CLI user's identity. The child process also receives the complete `process.env` object. A malicious target can read and transmit environment variables containing API tokens, cloud credentials, service configuration, or other secrets. No trust prompt, command allowlist, environment filtering, sandbox, or execution timeout is applied. This functionality is documented as target execution, so command execution is not covert. The security flaw is the absence of a trust boundary and safeguards when repository-controlled commands are exposed through an agent-callable MCP tool. ### Attack Path 1. An attacker supplies a repository containing a `.project/targets.json` file with a malicious command: ```json { "version": 1, "targets": { "test": { "description": "Run tests", "commands": [ "malicious-command-or-shell-payload" ] } } } ``` 2. The victim opens or operates inside that repository. 3. The user or agent invokes `project target run test`, or calls the MCP `project_targ ...[truncated 914 chars]
Remediation
## Remediation Suggestions 1. Establish a trusted-project policy. Do not execute targets from a newly encountered repository until the user explicitly marks the project or target configuration as trusted. 2. Display the exact command and working directory and require explicit confirmation before first execution or whenever `targets.json` changes. 3. Replace shell command strings with structured executable and argument arrays: ```json { "executable": "npm", "args": ["test"] } ``` Then invoke the executable with `shell: false`. 4. Validate executable names against an allowlist or organization policy where feasible. 5. Construct a minimal child environment instead of inheriting `process.env`. Remove API tokens, cloud credentials, SSH-related variables, and service secrets unless a target explicitly requires approved variables. 6. Run targets in a sandbox or container with restricted filesystem, network, process, and device access. 7. Apply execution timeouts and output limits to prevent resource exhaustion. 8. Record the target configuration digest and execution details in an audit log so changes are visible and attributable. 9. Clearly mark `project_target_run` as a privileged, side-effecting MCP operation rather than a routine read-only project action.

T07 · Tool Hijacking and Spoofing

Warning
Location
scripts/server.js:20
Finding
MCP Server Delegates Execution to an Unverified External CLI## Vulnerability Details **File Location**: `scripts/server.js:20-164` **Vulnerability Type**: External tool substitution and implementation mismatch **Risk Level**: Medium ### Vulnerable Code ```javascript const PROJECT_BIN = '/home/safa/clawd/bin/project'; ``` ```javascript function runProject(args) { if (!fs.existsSync(PROJECT_BIN)) { throw new Error(`project CLI not found at ${PROJECT_BIN}`); } const r = spawnSync(PROJECT_BIN, args, { encoding: 'utf8' }); const out = (r.stdout || '') + (r.stderr || ''); return { code: r.status ?? 1, out }; } ``` ### Technical Analysis The MCP server does not invoke the `scripts/project.js` implementation shipped in the audited package. Instead, it delegates all tool operations to a hard-coded executable at `/home/safa/clawd/bin/project`. The only validation performed is `fs.existsSync`, which confirms existence but not file type, ownership, permissions, provenance, content, or cryptographic integrity. If that external executable is replaced, modified, or redirected through a symbolic link, legitimate-looking MCP tool calls execute the substituted implementation. This also creates an auditability gap: reviewing the package's `scripts/project.js` does not establish what code the MCP server will execute at runtime. ### Attack Path 1. An attacker gains write access to `/home/safa/clawd/bin/project`, one of its parent directories, or a symlink used at that location. 2. The attacker replaces the expected CLI with a malicious executable or redirects the path to attacker-controlled code. 3. A client makes any supported MCP call, such as `project_detect`, `project_context_read`, or `project_target_run`. 4. `runProject` checks only that the external path exists. 5. The MCP server executes the substituted binary under the server account. 6. The malicious executable can return plausible tool output while performing unauthorized actions. Exploitation requir ...[truncated 757 chars]
Remediation
## Remediation Suggestions 1. Invoke the CLI bundled with the server by resolving it relative to `__dirname`: ```javascript const path = require('path'); const PROJECT_BIN = path.resolve(__dirname, 'project.js'); ``` 2. Prefer importing shared implementation functions directly rather than launching a separate executable. 3. If an external CLI is required, resolve its canonical path with `fs.realpathSync` and verify that it is a regular file, not a symbolic link. 4. Verify expected ownership and reject group-writable or world-writable executables and parent directories. 5. Pin and validate a cryptographic digest or signed release identity before execution. 6. Remove environment-specific hard-coded paths and use an explicit, validated deployment configuration. 7. Fail closed if identity or integrity validation cannot be completed. 8. Document which executable is used at runtime and include it in the same security review and release process as the MCP server.
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (8)

Description-Behavior Mismatch

High
Confidence
95% confidence
Finding
applyPlan() writes each plan entry to path.join(root, w.path) without validating that the resulting destination stays داخل the intended .project bundle or even within the project root. If an attacker can place or influence a plan file under .project/history/plans, they can use paths like ../../sensitive-file or absolute-like escapes to overwrite arbitrary files accessible to the current user.

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill exposes operational capabilities involving environment access and command-oriented workflows, but it does not declare any explicit tool scope such as allowed-tools or permissions. In a terminal-first skill that detects projects, reads workspace context, and invokes CLI/MCP actions, missing scope boundaries increases the chance of unintended tool or environment access beyond what users expect.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill explicitly supports running project-defined targets via `project target run <name>` and states that `project_target_run` executes commands from `.project/targets.json`, but it does not provide a strong user-facing warning that these commands may be arbitrary shell execution. Because `.project/targets.json` is project-local and may come from an untrusted repository, this creates a realistic path to executing attacker-controlled commands in the user's environment.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
Applying a plan performs filesystem writes immediately with no explicit warning or confirmation, which is especially risky because plan contents can include multiple file writes and are loaded from disk. In this skill's context, users may view plans as metadata artifacts, so silent application of changes makes social-engineering and accidental destructive writes more likely.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
runTarget() executes arbitrary command strings from .project/targets.json using spawnSync(..., { shell: true }), giving the target definition full shell execution with inherited environment and project-root cwd. In this skill context, target files are project-managed metadata, so a malicious repository or tampered .project bundle can turn a routine 'build/test/lint' action into arbitrary code execution.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The code runs shell commands from target definitions immediately and silently, with no explicit warning, dry-run, or confirmation step. Because this is a terminal-first project bootstrapper likely to be used on untrusted or newly cloned workspaces, the lack of user-visible execution warning materially increases the chance of accidental arbitrary command execution.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
`project_target_run` executes targets defined in `.project/targets.json`, which is project-controlled content and may contain arbitrary commands. Because the server exposes this as a simple tool call with no warning, validation, or approval step, an LLM agent operating on an untrusted repository could be induced to run attacker-supplied commands in the local environment.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The server exposes `project_plan_apply` directly and immediately executes `project apply <planId>` with no confirmation, policy gate, or dry-run enforcement. In an MCP context, this means an upstream agent or prompt-injected workflow can cause workspace modifications simply by invoking the tool, turning a planning primitive into an unguarded write/execute action.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/project.js:257

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/server.js:178