Back to skill

Security audit

Openclaw Memory

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it says, but its remote LLM observer can expose conversation text and may send the wrong API key to an external provider.

Review before installing or using the Observer. Prefer passing an explicit provider-specific apiKey, avoid relying on environment fallback, and do not process secrets or private conversations unless you are comfortable sending that text to the chosen LLM provider. Treat extracted facts as untrusted until reviewed before adding them to persistent memory.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • 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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
src/observer.ts:84
Finding
Provider-Agnostic API Key Fallback Can Disclose Credentials to the Wrong LLM Provider<![CDATA[ ## Vulnerability Details **File Location**: `src/observer.ts:84-129` **Vulnerability Type**: Cross-provider credential disclosure caused by insecure credential selection **Risk Level**: High ### Vulnerable Code ```typescript 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 — key in header, not URL (avoids log exposure) const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${this.config.model || 'gemini-2.0-flash'}:generateContent`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': key }, body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }), } ); const data = await res.json() as any; return data.candidates?.[0]?.content?.parts?.[0]?.text || ''; } ``` ### Technical Ana ...[truncated 2453 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Bind environment-variable selection strictly to the configured provider and reject missing credentials before issuing a request: ```typescript private getApiKey(): string { const environmentKey = this.config.provider === 'openai' ? process.env.OPENAI_API_KEY : this.config.provider === 'anthropic' ? process.env.ANTHROPIC_API_KEY : process.env.GEMINI_API_KEY; const key = this.config.apiKey || environmentKey; if (!key) { throw new Error(`Missing API key for provider: ${this.config.provider}`); } return key; } ``` Additional hardening measures: 1. Never fall back from one provider's environment variable to another provider's variable. 2. Document and support `GEMINI_API_KEY`. 3. Validate `provider` at runtime, particularly when configuration originates from untyped JavaScript or external data. 4. Ensure errors and diagnostic logs never include request headers or credential values. 5. Add tests confirming that each provider can access only its corresponding environment variable. 6. Add negative tests proving that a provider request is not sent when only another provider's key is available. 7. Prefer separately scoped, low-quota credentials for each provider to limit the impact of accidental disclosure. ]]>

T02 · Agent Memory Poisoning

Warning
Location
src/observer.ts:72
Finding
Untrusted Conversation Content Can Manipulate Extracted Memory Facts Through Prompt Injection<![CDATA[ ## Vulnerability Details **File Location**: `src/observer.ts:72-79` and `src/observer.ts:132-159` **Vulnerability Type**: Indirect prompt injection leading to potential memory poisoning **Risk Level**: Medium ### Vulnerable Code The conversation is concatenated directly with the extraction instructions: ```typescript async extract(messages: Message[], source = 'conversation'): Promise<Observation[]> { const sanitized = messages.map(m => `${m.role}: ${this.sanitize(m.content)}`).join('\n'); const prompt = EXTRACTION_PROMPT + sanitized; let response: string; try { response = await this.callLLM(prompt); } catch (err) { console.warn('[memory] LLM extraction failed:', (err as Error).message); return []; } return this.parseResponse(response, source); } ``` The model's output is then accepted based primarily on delimiter formatting: ```typescript private parseResponse(text: string, source: string): Observation[] { const kindMap: Record<string, ObservationKind> = { W: 'world', B: 'biographical', O: 'opinion', S: 'observation' }; const prioMap: Record<string, 'high' | 'medium' | 'low'> = { H: 'high', M: 'medium', L: 'low' }; const obs: Observation[] = []; for (const line of text.split('\n')) { const parts = line.split('|').map(s => s.trim()); if (parts.length < 4) continue; const kind = kindMap[parts[0]]; const priority = prioMap[parts[1]]; if (!kind || !priority) continue; const entities = parts[2].split(',').map(e => e.trim().replace(/^@/, '')).filter(Boolean); const content = parts.slice(3).join('|'); const confMatch = content.match(/confidence:\s*([\d.]+)/i); obs.push({ kind, timestamp: new Date(), entities, content: content.replace(/\(confidence:.*?\)/i, '').trim(), source, priority, confidence: confMatch ? parseFloat(confMatch[1]) : undefined, }); } return obs; } ``` ### Technical Analysis The `sanitize()` method removes ...[truncated 2847 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Send immutable extraction instructions using the provider's system-message mechanism rather than concatenating them with conversation data in a single user message. 2. Serialize conversation messages as structured JSON and place them inside explicit data delimiters. 3. State in the system instruction that any instructions contained inside conversation fields are untrusted data and must not be followed. 4. Require each extracted fact to include source message identifiers and exact supporting quotations. 5. Verify that supporting quotations exist verbatim in the submitted conversation before accepting a fact. 6. Reject outputs containing instruction-like or authorization-like claims unless explicitly supported by trusted source text. 7. Require human or policy approval before persisting high-priority, biographical, security-sensitive, or authorization-related observations. 8. Use schema-constrained output or provider-supported structured output instead of an ad hoc pipe-delimited format. 9. Validate all returned fields, including: - Maximum content and entity lengths - Maximum observation count - Allowed entity syntax - Finite confidence values restricted to `0.0` through `1.0` - Allowed source identifiers 10. Preserve provenance and treat extracted observations as untrusted assertions rather than established facts. 11. Add adversarial tests using visible prompt injections, delimiter manipulation, fabricated high-priority facts, and instructions embedded in different message roles. A safer processing design should separate extraction from persistence and require provenance validation before committing any observation to long-term memory. ]]>
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 (14)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding
The code in src/alma.ts is consistent with the ALMA portion of the description: it operates offline, evolves candidate designs through mutation, records evaluations, and ranks best results using a local database. However, the declared description presents a broader skill whose purpose includes agent memory, LLM fact extraction, full-text search, and remote API usage by an Observer. None of those behaviors appear in this supplied chunk. Because the actual code’s primary behavior is significantly narrower than the declared capability set, the description does not accurately represent what this code chunk actually does.

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The supplied code does not implement memory, meta-learning, fact extraction, full-text search, or any remote LLM API interaction. Its primary purpose is security scanning of text for suspicious Unicode characters associated with hidden prompt injection, including categorization, severity scoring, and decoding embedded tag payloads. This is a materially different function from the declared description, so the description does not accurately represent the code.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill metadata declares no explicit tool scope even though the description clearly indicates access to environment variables and outbound network use via remote LLM APIs. Missing permission declarations can cause reviewers or runtime policy systems to underestimate the skill's capabilities, increasing the risk of unintended secret access or data egress.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The Observer is described as sending conversation history to third-party LLM APIs, but the documentation does not provide a clear, prominent privacy warning or guidance on consent, minimization, and sensitive-data handling. If users or agents pass secrets, personal data, or confidential workspace content, that information may be transmitted off-device unexpectedly.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The observer sends full conversation content to third-party LLM APIs, which can expose sensitive user data, secrets, or personal information outside the local environment. In a memory/extraction skill, this is especially relevant because users may not realize their historical conversations are being transmitted off-box for processing.

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
78% confidence
Finding
The hardcoded OpenAI endpoint confirms an external recipient for conversation data, reinforcing the data egress risk already present at this call site. The danger is not the domain string itself, but that it represents transmission of user conversation content to a third-party processor.

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
78% confidence
Finding
The hardcoded OpenAI endpoint confirms an external recipient for conversation data, reinforcing the data egress risk already present at this call site. The danger is not the domain string itself, but that it represents transmission of user conversation content to a third-party processor.

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
78% confidence
Finding
The Anthropic endpoint string indicates remote transmission of potentially sensitive memory extraction input to a third party. In this skill context, where conversations are mined for structured facts, externalizing that text can leak personal or confidential information if users are not adequately informed or protected.

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
78% confidence
Finding
The Anthropic endpoint string indicates remote transmission of potentially sensitive memory extraction input to a third party. In this skill context, where conversations are mined for structured facts, externalizing that text can leak personal or confidential information if users are not adequately informed or protected.

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
79% confidence
Finding
The manifest includes vitest as an unpinned devDependency, and vitest has multiple known advisories affecting some versions. While this does not prove the installed version is vulnerable, the lack of pinning means a vulnerable release could be resolved in developer or CI environments, potentially exposing file read or code execution issues during testing workflows.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The code accesses provider API keys from process environment variables, which is a sensitive credential-handling operation under the warning criteria for code files. There is no nearby comment, disclosure, or user-visible notice explaining that the skill will read these credentials from the environment.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

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