Back to skill

Security audit

Nano Banana Openrouter

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but it handles API keys unsafely and can send the wrong provider's credential to OpenRouter.

Review before installing. Use only an OpenRouter-specific API key, remove or ignore the bundled test script with the hardcoded key, and do not include secrets, private data, or regulated content in image prompts because prompts are sent to OpenRouter. The GEMINI_API_KEY fallback should be removed before normal use.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
test-gen.mjs:3
Finding
Hard-Coded OpenRouter API Credential in Test Script<![CDATA[ ## Vulnerability Details **File Location**: `test-gen.mjs:3-12` **Vulnerability Type**: Plaintext hard-coded API credential **Risk Level**: High ### Vulnerable Code ```js import fetch from 'node-fetch'; const apiKey = "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"; async function generate() { console.log("Generating logo..."); try { const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, ``` ### Technical Analysis A plausible OpenRouter API key is embedded directly in source code and subsequently used as a bearer credential. Anyone who can read the source package, a copied archive, a published repository, or its revision history can recover the key without authentication. Bearer credentials are sufficient for access without proof of possession beyond knowledge of the token. Removing the key only from the current file would also be insufficient if it has already entered repository history, package caches, logs, or distributed artifacts. ### Attack Path 1. An attacker downloads or otherwise gains read access to the project. 2. The attacker inspects `test-gen.mjs` and extracts the plaintext token from line 3. 3. The attacker supplies the token in an `Authorization: Bearer ...` header to OpenRouter. 4. If the token remains active, the attacker submits requests under the associated OpenRouter account. 5. The attacker can continue consuming the account's quota or credits until the key is revoked, expires, or is restricted. ### Impact Assessment The exposed token may permit unauthorized use of the associated OpenRouter account within the permissions and spending limits assigned to that key. Potential effects include: - Unauthorized API requests and credit consumption. - Quota exhaustion and service disruption for the legitimate owner. - Activity attribution to the victim's account. - Access to any API c ...[truncated 213 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Revoke the exposed key immediately through the OpenRouter account dashboard and issue a replacement. 2. Remove the credential from the current source and all repository history using an appropriate history-rewriting tool. 3. Load test credentials exclusively from environment variables or a managed secret store: ```js const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { throw new Error("OPENROUTER_API_KEY is required"); } ``` 4. Add local secret files such as `.env` to `.gitignore`; provide only a non-sensitive `.env.example`. 5. Enable pre-commit and continuous-integration secret scanning. 6. Apply spending limits, model restrictions, rotation policies, and least-privilege controls to replacement credentials. 7. Review OpenRouter usage records for unauthorized requests made with the exposed key. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
index.ts:20
Finding
Gemini Credential Can Be Forwarded to OpenRouter<![CDATA[ ## Vulnerability Details **File Location**: `index.ts:20-33` **Vulnerability Type**: Cross-provider credential disclosure **Risk Level**: High ### Vulnerable Code ```ts const apiKey = process.env.OPENROUTER_API_KEY || process.env.GEMINI_API_KEY; // Fallback if (!apiKey) { throw new Error("Missing API Key. Set OPENROUTER_API_KEY in your environment or config."); } // OpenRouter Chat Completions Endpoint for Image Generation // Note: Gemini 2.5 Flash Image uses 'modalities: ["image"]' in the request body const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", "HTTP-Referer": "https://openclaw.ai", // Required by OpenRouter "X-Title": "OpenClaw Agent" }, ``` ### Technical Analysis The code silently falls back to `GEMINI_API_KEY` when `OPENROUTER_API_KEY` is unavailable. Regardless of which environment variable supplied the value, the selected credential is transmitted in the authorization header to `https://openrouter.ai`. A Gemini credential is intended for Google's API boundary, not OpenRouter. Forwarding it to another provider violates credential audience separation and unnecessarily discloses the secret to an unintended recipient. The fallback is also inconsistent with the error message and documented configuration, both of which instruct users to provide an OpenRouter credential. TLS protects the credential in transit from passive network interception, but it does not prevent the destination service or its request-processing infrastructure from receiving the credential. ### Attack Path 1. A deployment defines `GEMINI_API_KEY` for direct Google API use but does not define `OPENROUTER_API_KEY`. 2. A user or agent invokes `generateImage`. 3. The fallback expression selects the Gemini credential. 4. The skill sends that credential as a bearer token to OpenRouter. 5. The credential may become availa ...[truncated 853 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the cross-provider fallback and require an OpenRouter-specific credential: ```ts const apiKey = process.env.OPENROUTER_API_KEY; if (!apiKey) { throw new Error("Missing API key. Set OPENROUTER_API_KEY."); } ``` 2. Never infer that credentials for different providers are interchangeable. 3. If direct Gemini support is required, implement a separate code path with Google's official endpoint and explicitly select the matching credential. 4. Validate configuration at startup and fail closed before processing requests. 5. Rotate the Gemini key if this code has executed in an environment where only `GEMINI_API_KEY` was set. 6. Restrict provider keys by API, project, quota, billing limit, and any available network or application controls. 7. Review provider logs to determine whether a Gemini key was transmitted through this fallback. ]]>

T08 · Insecure Dependencies

Note
Location
package-lock.json:17
Finding
Dependency Lockfile Uses a Non-Default Registry Mirror<![CDATA[ ## Vulnerability Details **File Location**: `package-lock.json:17-22` **Vulnerability Type**: Additional dependency supply-chain trust boundary **Risk Level**: Low ### Vulnerable Code ```json "node_modules/@types/node": { "version": "20.19.33", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "dev": true, "license": "MIT", "dependencies": { ``` The same mirror is used for the other locked packages, including `node-fetch`, `typescript`, and their transitive dependencies. ### Technical Analysis The lockfile resolves packages through `registry.npmmirror.com` rather than the standard npm registry. This introduces another supply-chain trust boundary for package availability, metadata, and dependency refresh operations. The recorded SHA-512 integrity values materially reduce the likelihood that a modified archive can be substituted during an installation that strictly honors the lockfile. Consequently, the mirror reference alone does not prove package compromise. Risk remains during lockfile regeneration, dependency updates, integrity bypasses, or compromise of the configuration and metadata used to select future versions. ### Attack Path A conditional supply-chain exploitation path would be: 1. A developer or build process updates or regenerates dependencies while configured to use the mirror. 2. The mirror or related resolution configuration is compromised or serves manipulated package metadata. 3. A malicious artifact or version is selected and a new matching integrity value is written to the lockfile. 4. The modified lockfile is accepted without provenance review. 5. The malicious dependency executes during installation, build, import, or application runtime. No evidence in the audited files demonstrates that this attack has occurred. ### Impact Assessment If a dependency were compromised t ...[truncated 637 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Confirm whether use of `registry.npmmirror.com` is an intentional and governed organizational requirement. 2. If it is not required, configure npm to use `https://registry.npmjs.org/` and regenerate the lockfile from a trusted environment. 3. Review lockfile changes carefully, especially changes to `resolved`, `version`, and `integrity` fields. 4. Use deterministic installation with `npm ci` in continuous integration. 5. Pin and review dependency versions rather than accepting unreviewed updates. 6. Run dependency vulnerability, provenance, and license checks in CI. 7. If the mirror must remain in use, document it as an approved supply-chain dependency and monitor its security and availability. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • YARA SignaturesMalware Match, Webshell Match, Cryptominer Match
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (11)

YARA rule 'privilege_escalation_tools': Privilege escalation tools and techniques [hacktools]

High
Category
YARA Match
Content
XYwnOyen50rOPHh7Dv9ZNr8q6HD/kmwGcEeCzKEovtnctcZQ57Wns8Pd2yiwbzsmPnMY0TApF/VrIapM+cRqWkbxYJWACzqjzHeCwh7bOwOUJmaiGVVgfD4Gdq7542tTviKj49N5bdKSjq7kaRILKTzXebU2AR4+Sz+WTxtkPoCvEs/CQQCHRwYIfH8nOjdKROe7sSMdvxX9DmoudaHlxceY+e6c0pawIk/Rz+kqWg49UyV7dqK8Bi71Z+4fR4UyXhttLH2r1x02b+aq1QuZDA2InQixM8b+7dSNaJKdpmlXraEoqxFkvFVGdpS90ThnPd/nTuKnCXClB40Tr0Xqfx7SGEGJBLda4T71LG2d0WL7fT052Mr4YU5eHNhXI61RX0dWKcjy90iC2qmcpsPyTUYHvl7OxSPsvHNiiRSxfNjQjdruha2MPlD7F9tD5dKwlc2HjP/HRhIs/G3kwSj1/00sx3mQF9WZcu0xWbxdL/YrNglVVZWXBI2b3ZEyv0iS//yv6ZVVTKN1yOIrPYqA48YGwO216t9vZrDYR8viOW+PXNAtdXbOitWJTtRAo0Vq2iAFmuWIwyry+Gp8UDgdRgOYhldONdTo5v8GnRDvvZzNkIMh2+BK4z6lpIuasefIv1Zu1YDLEkORu4lt6Fz5gTXTJwRCkPtcBDzpEL2cM6oSdhlmrlsHKhr1gGvqKnOIhvgPQB7rkpWUtHOyfyBmLpcUCMrNbJYUf7I7h8ccrYMDpVZv39uXF9vwdefKxwIBAIdBE0P3AN1KmS65TCGDjEFShUSfiA4GbPBBR4LqZC0ukaPCA3rfw/uUI2fNKc6h1nDBE0eqSSTGWiazb3cvXaZSCoAyr57iqFXfaJ1zCs1lbeEdT1hBQB844onmP321Up5IBAIfCWODZWxvBnoQ2VF+OmGaEJvwRWHjzkD+4nkGQEPN1G2rNGu6RdmWy8Y5ueg6bzsAWDvsmrD6x1qeHYzg6ZIZOGQlHdJ6l9AH
Confidence
75% confidence
Finding
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).

External Transmission

Medium
Category
Data Exfiltration
Content
// OpenRouter Chat Completions Endpoint for Image Generation
  // Note: Gemini 2.5 Flash Image uses 'modalities: ["image"]' in the request body
  const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
Confidence
90% confidence
Finding
This code performs an outbound network request to a third-party API and includes user-controlled prompt content in the request body. External transmission is not inherently malicious, but in this skill context it becomes security-relevant because there is no validation, warning, or policy control around what data the agent may exfiltrate to the remote service.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The tool sends the user-supplied prompt, including any embedded sensitive or proprietary text, to OpenRouter without any disclosure in the tool metadata that prompt contents leave the local environment. In an agent setting, users may reasonably assume prompts are handled internally, so undisclosed third-party transmission creates a real privacy and data-handling risk even though the behavior appears functionally intended.

External Transmission

Medium
Category
Data Exfiltration
Content
async function generate() {
  console.log("Generating logo...");
  try {
    const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${apiKey}`,
Confidence
90% confidence
Finding
This code performs an external network transmission to a third-party API, which is security-relevant because it moves generated content and request metadata outside the local trust boundary. While external calls can be legitimate, in this file they are paired with a hardcoded credential and no guardrails around what may be transmitted.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The file makes a hardcoded outbound call to a third-party model API and embeds a live bearer credential directly in source. This creates two concrete risks: unauthorized external transmission of prompt content to an unvetted service and immediate credential compromise if the code is shared, logged, or committed to a repository.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill sends prompt content and an authorization credential to an external provider without any meaningful disclosure, consent flow, or data-handling notice beyond a generic log line. In agent contexts, this can cause users or operators to unknowingly exfiltrate sensitive business data, prompts, or proprietary content to a third party.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"start": "node index.js"
  },
  "dependencies": {
    "node-fetch": "^3.3.2"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
Confidence
94% confidence
Finding
The dependency uses a caret range (^3.3.2), which permits automatic installation of newer minor/patch releases. This creates supply-chain risk because a compromised or breaking upstream release could be pulled in without an explicit review, though the package itself is common and there is no direct evidence of malicious intent in this file.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"node-fetch": "^3.3.2"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}
Confidence
89% confidence
Finding
The development dependency @types/node is specified with a caret range, allowing unreviewed updates within the major version. While this is lower risk than a runtime package because it is primarily used during development/build time, it still introduces supply-chain exposure and build reproducibility issues.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  }
}
Confidence
90% confidence
Finding
The TypeScript compiler dependency is also unpinned via a caret range, so newer releases can be installed automatically. A compromised or incompatible compiler release could affect builds, generated output, or CI pipelines, even if it is not shipped directly at runtime.

Intent-Code Divergence

Low
Confidence
89% confidence
Finding
The comments at L11, L17, and L22 say the intent is to inspect the `output.json` structure and dump keys to locate image data. However, once image data is found, the script proceeds to decode base64 and write `logo.png` to disk at L33, which is a materially different side effect than the documented inspection/debugging intent.

Intent-Code Divergence

Low
Confidence
70% confidence
Finding
The inline console message frames the action as simply 'Generating logo...', but the actual request sends a detailed prompt to a remote multimodal model to design branded visual content. This is a mild intent/documentation divergence because the code is not merely performing a local logo-rendering step; it is delegating creative generation to an external service.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
index.ts:20

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
test-gen.mjs:3