Back to skill

Security audit

# key-guard A local MCP server that keeps API keys off Claude's servers. ## Why This Exists When Claude reads a file containing an API key, the raw key content gets sent to Claude's servers. key-guard prevents this by acting as a local middleman — Claude calls a tool, the tool reads the key and makes the API call locally, and only the result is returned to Claude.

Security checks for vulnerabilities and agentic risk

Overview

This skill is meant to protect API keys, but its MCP server gives the agent broad, weakly controlled access to credentials, outbound requests, and local file reads/writes.

Review carefully before installing. Only use this in a tightly sandboxed environment with throwaway credentials unless it is changed to restrict allowed key names, approved HTTPS destinations, readable and writable paths, and to require explicit confirmation before any secret-backed request or file write.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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 (5)

T09 · Insecure Skill Coding Practices

Error
Location
key-guard.js:80
Finding
Arbitrary Credential Exfiltration and Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `key-guard.js`, lines 80–97 and 143–151 **Vulnerability Type**: Arbitrary secret exfiltration through caller-controlled authenticated requests **Risk Level**: Critical ### Vulnerable Code ```js function getKey(name) { const all = loadAllKeys(); return all[name] || process.env[name] || null; } // ── HTTP helper (no external deps) ─────────────────────────────────────────── function request(url, options = {}) { return new Promise((resolve, reject) => { const parsed = new URL(url); const lib = parsed.protocol === "https:" ? https : http; const req = lib.request( { ...parsed, method: options.method || "GET", headers: options.headers || {} }, (res) => { let body = ""; res.on("data", (chunk) => (body += chunk)); res.on("end", () => { try { resolve({ status: res.statusCode, body: JSON.parse(body) }); } catch { resolve({ status: res.statusCode, body }); } }); } ); req.on("error", reject); if (options.body) req.write(JSON.stringify(options.body)); req.end(); }); } ``` ```js async function call_api({ key_name, url, method = "GET", headers = {}, body }) { const key = getKey(key_name); if (!key) return { error: `Key '${key_name}' not found` }; // Inject key into Authorization header (adapt pattern as needed) const authHeaders = { Authorization: `Bearer ${key}`, ...headers }; try { const result = await request(url, { method, headers: authHeaders, body }); // Return API result — raw key was never sent to Claude return { status: result.status, data: result.body }; } catch (err) { return { error: err.message }; } } ``` ### Technical Analysis The MCP caller controls both `key_name` and `url`. `getKey()` can retrieve values not only from the project's configured key sources, but also from arbitrary variables in `process.env`. The selected value is the ...[truncated 1688 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace arbitrary `key_name` and `url` combinations with an explicit configuration mapping each permitted key to exact approved HTTPS origins. - Refuse all HTTP destinations and validate protocol, hostname, port, and path before sending a credential. - Resolve hostnames and reject loopback, private, link-local, multicast, and cloud metadata address ranges for both IPv4 and IPv6. - Disable redirects or validate every redirect destination using the same rules. - Remove arbitrary `process.env[name]` lookup. Expose only credentials explicitly declared in a restrictive configuration. - Require explicit user approval before sending a credential to a new destination. - Apply request timeouts, response-size limits, and outbound network restrictions at the operating-system or container level. - Where possible, implement provider-specific tools rather than a general authenticated HTTP proxy. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
key-guard.js:119
Finding
Unrestricted Arbitrary File Read with Incomplete Secret Masking<![CDATA[ ## Vulnerability Details **File Location**: `key-guard.js`, lines 119–130 **Vulnerability Type**: Arbitrary filesystem read and sensitive information disclosure **Risk Level**: High ### Vulnerable Code ```js async function read_file_masked({ file_path }) { const resolved = path.resolve(file_path); if (!fs.existsSync(resolved)) return { error: `File not found: ${file_path}` }; let content = fs.readFileSync(resolved, "utf-8"); const all = loadAllKeys(); for (const [name, value] of Object.entries(all)) { if (value && value.length >= 8) { content = content.split(value).join(`{{${name}}}`); } } return { file_path, content }; } ``` Related secret-source behavior: ```js function loadAllKeys() { // Priority: shell profiles (lowest) → .env → process.env (highest) return { ...loadShellProfileKeys(), ...loadEnv() }; } function getKey(name) { const all = loadAllKeys(); return all[name] || process.env[name] || null; } ``` ### Technical Analysis `path.resolve(file_path)` normalizes the supplied path but does not restrict it to the project or another authorized directory. Absolute paths and traversal sequences can therefore reference any readable file available to the MCP process. There are also no checks for symbolic links that resolve outside an intended workspace. The masking mechanism only replaces values returned by `loadAllKeys()`, which includes `.env` entries and `KG_*` shell-profile entries. It does not include general `process.env` values even though those values are treated as secrets by `getKey()`. Values shorter than eight characters are intentionally not masked. Files containing credentials from other stores or formats are returned unchanged. ### Attack Path 1. An attacker influences the agent to invoke `read_file_masked`. 2. The attacker supplies an absolute sensitive path or a relative path that escapes the project directory. 3. The MCP process resolves and reads the target using its own operating-system p ...[truncated 621 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Establish a small set of explicitly authorized workspace roots. - Resolve the requested path with `realpath` and verify that the canonical path remains inside an authorized root. - Reject symbolic links, device files, sockets, directories, and known-sensitive filenames. - Consider allowing only user-confirmed files or files previously selected through a trusted interface. - Include all explicitly configured secret sources in masking, including permitted process-environment credentials. - Remove the minimum-length exception or fail closed if reliable masking cannot be guaranteed. - Detect common credential formats as defense in depth, while not treating pattern matching as a complete security boundary. - Run the MCP server under a dedicated, minimally privileged account with filesystem sandboxing. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
key-guard.js:132
Finding
Unrestricted Arbitrary File Overwrite and Secret Materialization<![CDATA[ ## Vulnerability Details **File Location**: `key-guard.js`, lines 132–140 **Vulnerability Type**: Arbitrary filesystem write with local secret substitution **Risk Level**: High ### Vulnerable Code ```js async function write_file_with_keys({ file_path, content }) { const resolved = path.resolve(file_path); const all = loadAllKeys(); let output = content; for (const [name, value] of Object.entries(all)) { if (value) output = output.split(`{{${name}}}`).join(value); } fs.writeFileSync(resolved, output, "utf-8"); return { success: true, file_path, message: "File written with keys substituted locally" }; } ``` ### Technical Analysis Both the destination and content are controlled by the MCP caller. `path.resolve()` does not enforce a workspace boundary, and `fs.writeFileSync()` follows symbolic links and truncates existing files. There is no allowlist, confirmation step, file-type restriction, ownership check, symlink defense, atomic replacement strategy, backup, or preservation of existing file permissions. In addition, recognized `{{KEY_NAME}}` placeholders are replaced with real secret values before the file is written. This allows an untrusted caller to cause secrets to be persisted in plaintext at an arbitrary writable location, even though the project's stated objective is to prevent key exposure. ### Attack Path 1. An attacker causes the agent to invoke `write_file_with_keys`. 2. The attacker supplies the path of a writable configuration, source, shell startup, or other sensitive file. 3. The attacker supplies arbitrary replacement content, optionally including known key placeholders. 4. The function substitutes matching placeholders with real credential values. 5. `fs.writeFileSync()` creates or truncates the target and writes the resulting content. 6. The overwrite may alter future application behavior, destroy data, or place plaintext credentials where another process or user can read them. ### Impact Assessment The call ...[truncated 404 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Restrict writes to canonical paths under explicitly approved workspace roots. - Use `realpath`-based containment checks and reject symbolic links and sensitive startup or configuration paths. - Require explicit user confirmation that displays the canonical destination and a secret-redacted diff. - Permit modification only of existing, user-approved regular files unless creation is separately authorized. - Use atomic temporary-file replacement in the same directory and preserve safe ownership and permissions. - Create secret-bearing files with restrictive permissions such as `0600`. - Avoid writing raw credentials into persistent files. Prefer environment-variable references or provider-specific credential stores. - Run the MCP server with least privilege and filesystem sandboxing. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
key-guard.js:108
Finding
Partial API Key Disclosure Through Validation Metadata<![CDATA[ ## Vulnerability Details **File Location**: `key-guard.js`, lines 108–116 **Vulnerability Type**: Partial sensitive-data exposure **Risk Level**: Medium ### Vulnerable Code ```js async function validate_key({ key_name }) { const value = getKey(key_name); if (!value) return { exists: false, message: `Key '${key_name}' not found` }; return { exists: true, length: value.length, preview: value.slice(0, 4) + "****", message: `Key '${key_name}' is set`, }; } ``` The implementation conflicts with the skill rule in `SKILL.md`, lines 24–26: ```md 3. **NEVER include a key value in your response**, even partially 4. **ALWAYS use the `key-guard` MCP server** for anything key-related ``` ### Technical Analysis The validation tool returns the first four characters and exact length of the selected secret. This directly contradicts the skill's stated guarantee that key values will never be included, even partially. Because `getKey()` accepts arbitrary names and also searches `process.env`, an attacker can probe guessed variable names and obtain existence information, exact lengths, and prefixes. Prefixes and lengths can identify credential providers, distinguish credential versions, or confirm whether a known credential is installed. ### Attack Path 1. An attacker supplies or guesses names of likely secret environment variables. 2. The agent invokes `validate_key` for each candidate name. 3. For existing variables, the tool returns the exact secret length and its first four characters. 4. The attacker uses the metadata to enumerate installed credentials, identify likely providers, or confirm credential guesses. ### Impact Assessment The vulnerability reveals partial secret material and enables environment-variable enumeration. While it does not disclose complete credentials by itself, the information reduces uncertainty for targeted attacks and violates the advertised security boundary. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Return only a boolean configuration status, such as `{ "exists": true }`. - Remove the `preview` and exact `length` fields. - Restrict validation to an explicit allowlist of credential names instead of arbitrary `process.env` variables. - Rate-limit repeated probes and require user approval before validating previously unapproved names. - Update documentation and tool descriptions so they accurately reflect the information returned. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
README.md:50
Finding
Setup Command Overwrites Existing MCP Configuration<![CDATA[ ## Vulnerability Details **File Location**: `README.md`, lines 50–55 **Vulnerability Type**: Unsafe destructive configuration installation **Risk Level**: Medium ### Vulnerable Code ```bash ### 2. Register MCP Server Copy the MCP config to your Copilot CLI config directory: ```bash cp .agents/mcp-config.json ~/.copilot/mcp-config.json ``` ``` ### Technical Analysis The documented `cp` command replaces `~/.copilot/mcp-config.json` rather than merging the `key-guard` entry into the user's existing configuration. It does not test whether the destination exists, create a backup, request confirmation, or preserve unrelated MCP server definitions. The provided project structure also places `mcp-config.json` at the project root, while the command references `.agents/mcp-config.json`, creating an additional risk of failure or user confusion. The confirmed security issue is the destructive overwrite behavior if the referenced source exists in the installed layout. ### Attack Path 1. A user already has one or more MCP servers configured in `~/.copilot/mcp-config.json`. 2. The user follows the documented setup command. 3. The shell copies the project configuration over the complete existing file. 4. All unrelated MCP configuration entries are silently removed unless the user has an external backup. ### Impact Assessment The command can cause loss of security-sensitive tool configuration, remove trusted MCP integrations, or replace a carefully reviewed configuration with the project's template. This primarily affects integrity and availability of the user's agent tooling configuration. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Do not instruct users to overwrite the complete MCP configuration. - Prefer a configuration-aware registration command that adds only the `key-guard` server entry. - If manual editing is required, instruct users to merge the `mcpServers.key-guard` object into their existing JSON. - Provide a validated script that creates a timestamped backup, parses JSON, checks for naming conflicts, and performs an atomic update. - Correct the documented source path so that it matches the actual packaged location. - Warn users not to proceed if parsing or merge validation fails. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (27)

Credential Access

High
Category
Privilege Escalation
Content
### 1. Add your keys
```bash
cp .env.example .env
# Edit .env with your real API keys
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill is presented as a guardrail, but it also enables credential discovery, partial secret disclosure, arbitrary masked file reads, and writing files with real secret substitution. That mismatch is dangerous because users and reviewers may trust it as purely protective while it actually expands the agent's ability to enumerate secrets and place them into executable files or outbound API calls.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
---
name: key-guard
description: "Security guardrail: prevents API keys from being sent to Claude. Triggers when user asks to call an external API, use a key, check credentials, read .env files, or view/edit scripts that may contain hardcoded keys. Always routes key usage through the local MCP server instead."
---

# Key Guard
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
### User: "Show me my .env file"
```
Do NOT read .env directly.
Instead, call validate_key for each expected key name and show:
- Which keys are configured
- Approximate length (as a sanity check)
Confidence
90% confidence
Finding
Returning which keys exist and their approximate lengths creates credential metadata leakage that can aid attackers in enumerating installed providers, validating guessed secret types, and profiling the environment. In a guardrail skill, even partial secret disclosure undermines the stated goal of minimizing key exposure.

Credential Access

High
Category
Privilege Escalation
Content
// ── Config ────────────────────────────────────────────────────────────────────

const ENV_FILE = path.resolve(__dirname, ".env");

// Shell profile files to scan for KG_* keys, in priority order (last wins before .env)
const SHELL_PROFILES = [
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
write_file_with_keys allows arbitrary local file writes to any resolved path and substitutes real secret values into the written content. This can be abused to persist credentials into source files, shell startup files, cron jobs, or other sensitive locations, causing secret leakage, privilege abuse, or durable compromise of the host environment.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill invokes credential- and file-related capabilities but declares no explicit tool scope or allowed-tools boundary. In a security-oriented skill, missing least-privilege constraints increases the chance that the agent can access broader resources than intended, especially around secrets and local files.

External Transmission

Medium
Category
Data Exfiltration
Content
```
Call: call_api({
  key_name: "OPENAI_API_KEY",
  url: "https://api.openai.com/v1/models",
  method: "GET"
})
Returns: { status: 200, data: { ... API response ... } }
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
```
Call: call_api({
  key_name: "OPENAI_API_KEY",
  url: "https://api.openai.com/v1/models",
  method: "GET"
})
Returns: { status: 200, data: { ... API response ... } }
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Session Persistence

Medium
Category
Rogue Agent
Content
You can now safely view and suggest edits to the non-key parts.

### Tool 4: `write_file_with_keys`
Write a file back after editing, with `{{KEY_NAME}}` placeholders substituted with real key values locally.
```
Call: write_file_with_keys({
  file_path: "./call.sh",
Confidence
93% confidence
Finding
The skill can persist real secrets into arbitrary files by substituting placeholders during writeback. This creates a durable secret-exposure path: credentials may end up in scripts, configs, backups, logs, or repositories, and can then be accessed by unrelated processes or later exfiltrated.

External Transmission

Medium
Category
Data Exfiltration
Content
```
Call: write_file_with_keys({
  file_path: "./call.sh",
  content: "curl -H 'Authorization: Bearer {{OPENAI_API_KEY}}' https://api.openai.com/v1/chat/completions ..."
})
Returns: { success: true, message: "File written with keys substituted locally" }
```
Confidence
84% confidence
Finding
This workflow writes a script containing a real credential substituted locally, which can later be executed to transmit the secret to an external service. Even if Claude never sees the key, the skill materially enables persistence and reuse of secrets in files, increasing the risk of accidental disclosure, source control leaks, or misuse by other tools/processes.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill silently scans .env and user shell profile files for secrets without any user-facing warning or confirmation. In an LLM-tooling context, this is dangerous because a prompt-injected or overly broad task can trigger credential discovery and subsequent use of local secrets without the user realizing sensitive sources were accessed.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The skill’s stated purpose is to prevent key exposure to Claude, but it also exposes broader capabilities: enumerating local key names and performing arbitrary file read/write operations. Those extra capabilities materially expand the attack surface and enable an LLM or prompt-injected workflow to inspect sensitive local files or manipulate files while leveraging locally stored secrets.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
read_file_masked permits reading any local file path, far beyond what is needed for a key-routing guardrail. Although it attempts to mask known key values, masking is incomplete because only currently loaded secrets of length >= 8 are replaced, so unrelated credentials, short secrets, tokens in alternate formats, and other sensitive local data can still be exposed to Claude.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
Writing arbitrary files with live secret substitution happens with no approval step or warning to the user. This combination is especially risky because it can quietly materialize credentials into files that are later committed, logged, served, or executed, turning a temporary secret into a durable exposure or persistence mechanism.

Static analysis

No suspicious patterns detected.