Back to skill

Security audit

Openclaw Memories

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated memory purpose, but a real credential-handling flaw can send an API key to the wrong LLM provider.

Review before installing or using in a sensitive environment. Use explicit provider-specific apiKey values, avoid relying on environment-variable fallback, and do not pass private conversation history to Observer unless the selected LLM provider is approved for that data. ALMA and Indexer appear suitable for offline use.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Error
Location
src/observer.ts:67
Finding
Provider-Agnostic API-Key Fallback Can Disclose Credentials to the Wrong LLM Provider## Vulnerability Details **File Location**: `src/observer.ts`, lines 67–108 **Vulnerability Type**: Cross-provider credential disclosure caused by insecure credential selection **Risk Level**: High ### Vulnerable Code ```ts private async callLLM(prompt: string): Promise<string> { const key = this.config.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || ''; if (this.config.provider === 'openai') { const res = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` }, body: JSON.stringify({ model: this.config.model || 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], max_tokens: 2000, }), }); const data = await res.json() as any; return data.choices?.[0]?.message?.content || ''; } if (this.config.provider === 'anthropic') { const res = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': key, 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: this.config.model || 'claude-3-5-haiku-20241022', max_tokens: 2000, messages: [{ role: 'user', content: prompt }], }), }); const data = await res.json() as any; return data.content?.[0]?.text || ''; } // Gemini const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model || 'gemini-2.0-flash'}:generateContent?key=${key}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }), } ); const data = await res.json() as any; return data.candidates?.[0]?.content?.parts?.[0]?.text || ''; } ``` ### Technical Analysis The credential-selection logic is independent of the confi ...[truncated 3177 chars]
Remediation
## Remediation Suggestions 1. Select credentials strictly according to the configured provider: ```ts private getApiKey(): string { if (this.config.apiKey) return this.config.apiKey; switch (this.config.provider) { case 'openai': return process.env.OPENAI_API_KEY || ''; case 'anthropic': return process.env.ANTHROPIC_API_KEY || ''; case 'gemini': return process.env.GEMINI_API_KEY || ''; } } ``` 2. Reject missing credentials before constructing or sending a request: ```ts const key = this.getApiKey(); if (!key) { throw new Error(`Missing API key for provider: ${this.config.provider}`); } ``` 3. Never fall back to another provider's environment variable. 4. Use an authentication header for Gemini if supported by the selected API interface. If the API requires a query parameter, ensure request URLs are redacted from logs, telemetry, and exception messages. 5. Validate the provider and model combination so that unsupported or unexpected configuration fails before any sensitive data is transmitted. 6. Add mocked unit tests that verify: - OpenAI receives only an explicit key or `OPENAI_API_KEY`. - Anthropic receives only an explicit key or `ANTHROPIC_API_KEY`. - Gemini receives only an explicit key or `GEMINI_API_KEY`. - Missing provider-specific credentials cause a local failure without a network request. - Test runs never contact real provider endpoints. 7. Document `GEMINI_API_KEY` alongside the other supported environment variables and explain that conversation content is sent to the selected provider.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • 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 (13)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The code accurately matches the 'ALMA meta-learning' portion of the description: it mutates parameter sets, evaluates them, persists results locally, and tracks best-performing designs. However, the declared purpose describes a larger skill that also includes LLM fact extraction, full-text search, and an observer that calls remote model APIs. None of those behaviors appear in this chunk. Because the actual code is a narrower offline optimization/database module rather than the broader described memory system, the description does not accurately represent what this supplied code chunk actually does.

Lp3

Medium
Category
MCP Least Privilege
Confidence
87% confidence
Finding
The skill metadata advertises capabilities that involve environment-variable access and remote network use, but it does not declare any explicit tool scope or permissions boundaries. In an agent ecosystem, this weakens reviewability and can cause the skill to be granted broader access than users expect, increasing the chance of secret exposure or unintended outbound requests.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The code concatenates full conversation messages into a prompt and sends them to third-party LLM providers without any in-code consent gate, redaction step, or user-facing disclosure. Because the extracted target data explicitly includes biographical facts and opinions, sensitive user-provided content can be transmitted off-device/off-service in a way users may not expect.

Ssd 3

Medium
Confidence
98% confidence
Finding
`extract()` forwards the entire conversation text to an external LLM to derive memory facts, including biographical and opinion content. This creates a direct natural-language exfiltration path for sensitive information embedded anywhere in the chat, even if that information was never intended to be stored or shared externally.

External Transmission

Medium
Category
Data Exfiltration
Content
const key = this.config.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || '';

    if (this.config.provider === 'openai') {
      const res = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
        body: JSON.stringify({
Confidence
93% confidence
Finding
This network call transmits prompt contents to the OpenAI API, and the prompt is built from full conversation messages. In the context of a memory skill, that means potentially sensitive personal or confidential chat data is sent to an external service, making privacy leakage the main risk.

External Transmission

Medium
Category
Data Exfiltration
Content
const key = this.config.apiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || '';

    if (this.config.provider === 'openai') {
      const res = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
        body: JSON.stringify({
Confidence
93% confidence
Finding
This network call transmits prompt contents to the OpenAI API, and the prompt is built from full conversation messages. In the context of a memory skill, that means potentially sensitive personal or confidential chat data is sent to an external service, making privacy leakage the main risk.

External Transmission

Medium
Category
Data Exfiltration
Content
}

    if (this.config.provider === 'anthropic') {
      const res = await fetch('https://api.anthropic.com/v1/messages', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
Confidence
93% confidence
Finding
This request sends the assembled conversation prompt to Anthropic's API, again creating an external transmission path for all included chat content. The danger is increased by the skill's purpose of extracting memory from personal conversations, which naturally encourages processing of private user details.

External Transmission

Medium
Category
Data Exfiltration
Content
}

    if (this.config.provider === 'anthropic') {
      const res = await fetch('https://api.anthropic.com/v1/messages', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
Confidence
93% confidence
Finding
This request sends the assembled conversation prompt to Anthropic's API, again creating an external transmission path for all included chat content. The danger is increased by the skill's purpose of extracting memory from personal conversations, which naturally encourages processing of private user details.

Unpinned Dependencies

Low
Category
Supply Chain
Content
],
  "dependencies": {},
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "vitest": "^1.0.0"
  }
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"dependencies": {},
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "vitest": "^1.0.0"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0",
    "vitest": "^1.0.0"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Unverifiable Dependency: vitest has 3 known advisory(ies) (CVE-2026-47429 (When Vitest UI server is listening, arbitrary file can be read and executed); CVE-2026-84373 (Vitest: Path Traversal / Arbitrary File Read via @vitest/mocker Redirect Mock); CVE-2025-24964 (Vitest allows Remote Code Execution when accessing a malicious website while Vit)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
81% confidence
Finding
The manifest references Vitest with an unpinned range while known advisories exist for some Vitest releases, so the installed version could be vulnerable depending on resolution time. Although Vitest is a devDependency rather than a runtime dependency, vulnerable test tooling can still expose developers or CI systems to file read or code execution risks during local testing or automated pipelines.

Missing User Warnings

Low
Confidence
76% confidence
Finding
The cleanup hook removes `testDir` with `rmSync(..., { recursive: true, force: true })`, which is a destructive filesystem operation. In this file there is no confirmation, log, or comment disclosing that the test will delete files under the generated temporary directory.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/observer.ts:68