Back to skill

Security audit

Operrouter Connect

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly aligned with using OpenRouter free models, but it reads broad credential sources and uses an unsafe shared temporary cache, so users should review it before installing.

Install only if you are comfortable with the agent reading .env files, using an OpenRouter API key, sending prompts to OpenRouter, and writing a local model cache. Prefer exporting only OPENROUTER_API_KEY for the process, avoid storing unrelated secrets in ~/.env, pass prompts through stdin instead of --prompt, and harden or disable the shared /tmp cache before using this around sensitive data.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/resolve_key.py:29
Finding

Overbroad Loading of Unrelated Secrets from Environment Files

Content
View full analysis
dict: env = {} env.update(load_env_file(Path.home() / ".env")) env.update(load_env_file(Path(".env"))) env.update(os.environ) return env def get_key(env: dict) -> str: key = env.get("OPENROUTER_API_KEY", "").strip() ``` ```typescript // references/typescript_template.md:57-67 function resolveEnv(): Record { // 1. Project .env 2. ~/.env 3. process.env (already exported) const projectEnv = loadEnvFile(path.join(process.cwd(), ".env")); const globalEnv = loadEnvFile(path.join(os.homedir(), ".env")); // Later entries win — process.env takes precedence return { ...globalEnv, ...projectEnv, ...process.env as Record }; } const ENV = resolveEnv(); function getApiKey(): string { const key = ENV["OPENROUTER_API_KEY"]?.trim(); ``` ### Technical Analysis The Skill requires only a limited set of OpenRouter-related values, principally `OPENROUTER_API_KEY`, `OPENROUTER_PREFERRED_MODELS`, `OPENROUTER_TIER_A`, and `OPENROUTER_TIER_B`. Instead, the implementations parse every entry in the project `.env`, the global `~/.env`, and the complete process environment into Skill-controlled dictionaries. Global `.env` files commonly contain unrelated da ...[truncated 1370 chars]
Remediation
View remediation
dict[str, str]: values = load_env_file(path) return {key: values[key] for key in ALLOWED_KEYS if key in values} ``` - Avoid retaining the API key longer than necessary and never include it in logs, exceptions, or diagnostic state. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/discover_models.py:17
Finding

Shared Predictable Temporary Cache Enables Cache Poisoning and Symlink File Overwrite

Content
View full analysis
list: if not force_refresh and CACHE_FILE.exists(): age = time.time() - CACHE_FILE.stat().st_mtime if age < CACHE_TTL: return json.loads(CACHE_FILE.read_text()) print("[openrouter-connect] Fetching model list from OpenRouter...", file=sys.stderr) with urllib.request.urlopen(MODELS_URL, timeout=10) as resp: all_models = json.loads(resp.read())["data"] free = [ m for m in all_models if m.get("pricing", {}).get("prompt") == "0" and m.get("pricing", {}).get("completion") == "0" ] CACHE_FILE.write_text(json.dumps(free)) ``` The authenticated proxy trusts the same cache: ```python # scripts/proxy_query.py:88-101 def fetch_free_models(force: bool = False) -> list: if not force and CACHE_FILE.exists(): if time.time() - CACHE_FILE.stat().st_mtime < CACHE_TTL: return json.loads(CACHE_FILE.read_text()) with urllib.request.urlopen(MODELS_ENDPOINT, timeout=10) as r: data = json.loads(r.read())["data"] free = [m for m in data if m.get("pricing", {}).get("prompt") == "0" and m.get("pricing", {}).get("completion") == "0"] CACHE_FILE.write_text(json.dumps(free)) return free ``` ### Technical Analysis The cache uses a fixed pathname in a commonly shared temporary directory. The code does not verify: - File ownership or permissions. - Whether the path is ...[truncated 2259 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:80
Finding

Sensitive User Prompts Are Passed Through Process Command-Line Arguments

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
references/python_template.md:252
Finding

Generated Workflows Install and Execute Unpinned Third-Party Dependencies

Content
View full analysis
=1.0 works with OpenRouter except ImportError: raise SystemExit("Run: pip install openai python-dotenv") ``` ```text # references/python_template.md:252-255 ## Dependencies pip install openai python-dotenv ``` ```typescript // references/typescript_template.md:7-14 // Run: npx ts-node openrouter_connect_client.ts // Or: node openrouter_connect_client.js (after tsc) import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import OpenAI from "openai"; // npm install openai dotenv ``` ```bash # references/typescript_template.md:215-220 npm install openai # TypeScript only: npm install -D typescript ts-node @types/node ``` ### Technical Analysis The templates instruct users to resolve and execute mutable latest versions of packages without exact version pins, lockfiles, package hashes, or integrity verification. Package installation and execution occur in a process that can access the user's files, environment, API key, and prompts. The `npx ts-node` workflow also creates a risk of implicit package retrieval if the expected local executable is unavailable. Package lifecycle scripts may execute during npm installation unless explicitly disabled. No malicious package name or confirmed compromised dependency was found. The vulnerability is the lack of controls against future package compromise, unexpected breaking changes, registry substitution, or compromised transitive dependencies. ### Attack Path 1. A user follows the generated dependency-installation instructions. 2. The package manager resolves the latest available direct and transitive dependency versions. 3. A compr ...[truncated 768 chars]
Remediation
View remediation
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • 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 (39)

Tp4

High
Category
MCP Tool Poisoning
Confidence
92% confidence
Finding

The declared description promises a broad skill for working with OpenRouter free models, including routing prompts through free models, comparing outputs, testing models, and generating code to call OpenRouter. The supplied code does something narrower: it only queries the OpenRouter models endpoint, filters free models, ranks them, caches the list, and optionally outputs model IDs as JSON. While this supports one part of the description—picking free OpenRouter models—it does not implement the broader primary behaviors described. This is a material description/behavior mismatch because the actual capability is model discovery/ranking only, not general free-tier LLM routing or code scaffolding.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/python_template.md (reported line 28)May include surrounding context.

md
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/python_template.md (reported line 73)May include surrounding context.

md
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/typescript_template.md (reported line 38)May include surrounding context.

md
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/proxy_query.py (reported line 77)May include surrounding context.

python
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/resolve_key.py (reported line 29)May include surrounding context.

python
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/resolve_key.py (reported line 32)May include surrounding context.

python
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/resolve_key.py (reported line 43)May include surrounding context.

python
# ── Config ────────────────────────────────────────────────────────────────────

# Ranked preference list — tried in order, skipping unavailable free models.
# Override at runtime via OPENROUTER_PREFERRED_MODELS=a,b,c in your .env
PREFERRED_MODELS: list[str] = [
    # ── Tier 1: Qwen ──────────────────────────────────────
    "qwen/qwen-2.5-72b-instruct:free",

Credential Access

High
Category
Privilege Escalation
Confidence
74% confidence
Finding

The function explicitly searches both the project .env and the user's global ~/.env for an API key. In a skill/template context, reading a global secrets file expands the trust boundary and may encourage code that accesses unrelated user secrets beyond the project scope, which is riskier than requiring explicit environment injection.

Content

Scanner excerpt · references/python_template.md (reported line 55)May include surrounding context.

md
"""
    Look for OPENROUTER_API_KEY in:
      1. ./env  (project root)
      2. ~/.env (global fallback)
      3. Already-exported shell environment
    Raises RuntimeError if not found.
    """

Credential Access

High
Category
Privilege Escalation
Confidence
86% confidence
Finding

Automatically loading credentials from a local .env file causes the code to ingest secrets from disk without an explicit consent step at runtime. In an agent skill whose purpose is to relay prompts externally, implicit secret loading increases the chance of unsafe reuse of developer credentials and broadens the impact of accidental invocation.

Content

Scanner excerpt · references/python_template.md (reported line 59)May include surrounding context.

md
3. Already-exported shell environment
    Raises RuntimeError if not found.
    """
    # 1. Project .env
    project_env = Path(".env")
    if project_env.exists():
        load_dotenv(project_env, override=False)

Credential Access

High
Category
Privilege Escalation
Confidence
90% confidence
Finding

Loading credentials from ~/.env reaches into a user-wide secret store that may contain unrelated tokens and creates a broader-than-necessary secret access pattern. In a skill that forwards prompts to external providers, this increases the danger of unauthorized secret use if the template is adopted unchanged or invoked unexpectedly in shared or agentic environments.

Content

Scanner excerpt · references/python_template.md (reported line 64)May include surrounding context.

md
if project_env.exists():
        load_dotenv(project_env, override=False)

    # 2. Global ~/.env
    global_env = Path.home() / ".env"
    if global_env.exists():
        load_dotenv(global_env, override=False)

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

This code explicitly reads a global ~/.env, which can contain many unrelated secrets beyond the OpenRouter API key. In a reusable template, this broad credential access is dangerous because it normalizes code that consumes secrets from a location users may not expect, increasing accidental exposure and misuse risk.

Content

Scanner excerpt · references/typescript_template.md (reported line 57)May include surrounding context.

md
}

function resolveEnv(): Record<string, string> {
  // 1. Project .env  2. ~/.env  3. process.env (already exported)
  const projectEnv = loadEnvFile(path.join(process.cwd(), ".env"));
  const globalEnv  = loadEnvFile(path.join(os.homedir(), ".env"));
  // Later entries win — process.env takes precedence

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

The specific statement loadEnvFile(path.join(os.homedir(), ".env")) accesses a home-directory secret store unrelated to the current project. That widens the blast radius if the generated code is run on a developer workstation containing other credentials, and the skill context makes this more concerning because users may execute scaffolds with minimal review.

Content

Scanner excerpt · references/typescript_template.md (reported line 58)May include surrounding context.

md
function resolveEnv(): Record<string, string> {
  // 1. Project .env  2. ~/.env  3. process.env (already exported)
  const projectEnv = loadEnvFile(path.join(process.cwd(), ".env"));
  const globalEnv  = loadEnvFile(path.join(os.homedir(), ".env"));
  // Later entries win — process.env takes precedence
  return { ...globalEnv, ...projectEnv, ...process.env as Record<string, string> };

Credential Access

High
Category
Privilege Escalation
Confidence
91% confidence
Finding

Merging globalEnv, projectEnv, and process.env into a single object compounds the broad secret-ingestion issue by making all discovered environment values available to the program. While this template only uses OPENROUTER_API_KEY directly, the pattern encourages over-collection of credentials and increases the chance future modifications expose or misuse them.

Content

Scanner excerpt · references/typescript_template.md (reported line 59)May include surrounding context.

md
function resolveEnv(): Record<string, string> {
  // 1. Project .env  2. ~/.env  3. process.env (already exported)
  const projectEnv = loadEnvFile(path.join(process.cwd(), ".env"));
  const globalEnv  = loadEnvFile(path.join(os.homedir(), ".env"));
  // Later entries win — process.env takes precedence
  return { ...globalEnv, ...projectEnv, ...process.env as Record<string, string> };
}

Credential Access

High
Category
Privilege Escalation
Confidence
86% confidence
Finding

The error message instructs users to place the API key in either ./.env or ~/.env, reinforcing the unsafe global secret-loading behavior. In a skill template, prescriptive guidance matters because users often follow it verbatim, which can institutionalize insecure credential storage practices.

Content

Scanner excerpt · references/typescript_template.md (reported line 70)May include surrounding context.

md
const key = ENV["OPENROUTER_API_KEY"]?.trim();
  if (!key) throw new Error(
    "OPENROUTER_API_KEY not found.\n" +
    "Add it to ./.env or ~/.env:\n\n" +
    "  OPENROUTER_API_KEY=sk-or-...\n\n" +
    "Get a free key at https://openrouter.ai/keys"
  );

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/python_template.md (reported line 60)May include surrounding context.

md
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · references/python_template.md (reported line 65)May include surrounding context.

md
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/discover_models.py (reported line 36)May include surrounding context.

python
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/discover_models.py (reported line 37)May include surrounding context.

python
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/proxy_query.py (reported line 66)May include surrounding context.

python
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/proxy_query.py (reported line 67)May include surrounding context.

python
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/resolve_key.py (reported line 30)May include surrounding context.

python
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/resolve_key.py (reported line 33)May include surrounding context.

python
env = {}

    # 1. Global ~/.env
    env.update(load_dotenv_simple(Path.home() / ".env"))

    # 2. Project .env (overrides global)
    env.update(load_dotenv_simple(Path(".env")))

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill instructs the agent to read local files, access environment variables, and make network calls, but it declares no explicit tool scope or permission boundaries. That increases the chance of unintended credential access or outbound requests occurring without clear review or least-privilege constraints.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The trigger conditions are extremely broad, including any mention of OpenRouter, free models, proxying, code generation, model selection, or comparison. Over-broad activation can cause the skill to run in contexts where the user did not intend file reads, credential resolution, or network activity, increasing the chance of unnecessary sensitive actions.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.