Back to skill

Security audit

Storage Private — Encrypted Multi-Node Agent Storage

Security checks for vulnerabilities and agentic risk

Overview

The skill is coherently built for encrypted storage, but it uses high-impact wallet secrets and automatically trusts HTTP-discovered storage nodes, so it needs careful review before use.

Install only if you are comfortable with encrypted data and metadata being replicated to configured and discovered remote nodes. Prefer a dedicated STORAGE_PRIVATE_KEY, review or disable discovered nodes before storing sensitive data, avoid descriptive key names, and use trusted HTTPS/authenticated storage endpoints where possible.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/storage.mjs:136
Finding
Sensitive storage metadata and encrypted objects are transmitted over unauthenticated plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/storage.mjs:136-177`; affected endpoints are configured in `config.json:19-64` **Vulnerability Type**: Plaintext transmission of sensitive metadata and unauthenticated encrypted storage objects **Risk Level**: High ### Evidence ```javascript 'memory-store': { async write(node, agentId, cid, envelopeBytes, metadata) { const r = await httpJson(`${node.url}/api/v1/agent/${agentId}/memory`, { method: 'PUT', body: JSON.stringify({ content: Buffer.from(envelopeBytes).toString('base64'), type: 'encrypted-blob', filename: `${metadata.key}.encrypted`, timestamp: Math.floor(Date.now() / 1000), }), }); if (!r.ok) throw new Error(`HTTP ${r.status}: ${JSON.stringify(r.data)}`); return { cid: r.data.cid, nodeId: node.id }; }, ``` ```javascript 'filstream': { async write(node, agentId, cid, envelopeBytes, metadata) { // Upload encrypted blob to FilStream index → automatically distributed to seeders const boundary = '----StoragePrivate' + Date.now(); const filename = `${agentId}/${metadata.key}.encrypted`; const title = `[encrypted] ${metadata.key}`; const bodyParts = [ `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: application/octet-stream\r\n\r\n`, envelopeBytes, `\r\n--${boundary}\r\nContent-Disposition: form-data; name="title"\r\n\r\n${title}`, `\r\n--${boundary}--\r\n`, ]; const body = Buffer.concat(bodyParts.map(p => typeof p === 'string' ? Buffer.from(p) : p)); const resp = await fetch(`${node.index_url}/api/upload`, { method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, body, }); ``` Representative configured endpoints: ```json { "id": "norway-primary", "type": "memory-store", "url": "http://[2a05:a00:2::10:11]:8081", "enabled": true }, { "id": "filstr ...[truncated 3019 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require HTTPS for every non-local backend and reject plaintext HTTP endpoints by default. 2. Validate TLS certificates and hostnames; for sensitive deployments, support certificate or public-key pinning. 3. Authenticate requests using scoped credentials or request signatures. Bind signatures to the method, path, body hash, timestamp, and nonce to prevent replay. 4. Replace raw namespaces and object keys in remote identifiers with keyed pseudonyms, such as an HMAC computed with a dedicated metadata key. 5. Remove descriptive multipart titles and filenames. Use random or content-addressed opaque identifiers. 6. Document unavoidable metadata leakage, including encrypted sizes, timing, destination nodes, and access patterns. 7. Validate response envelopes locally and bind stable metadata as AEAD additional authenticated data. 8. Refuse insecure endpoints unless the user supplies an explicit development-only override with a prominent warning. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/discover.mjs:144
Finding
Unauthenticated discovery automatically authorizes untrusted servers as replication targets<![CDATA[ ## Vulnerability Details **File Location**: `scripts/discover.mjs:53-103`, `scripts/discover.mjs:144-156`, and `scripts/discover.mjs:189-200`; automatic replication occurs at `scripts/storage.mjs:302-307` **Vulnerability Type**: Trust-boundary violation through unauthenticated node discovery and automatic enablement **Risk Level**: High ### Evidence The discovery process trusts a remote HTTP registry: ```javascript // Step 1: Get all seeders from the index const seedersResp = await probe(`${INDEX_URL}/api/seeders`, 10000); if (!seedersResp.ok) { console.error(`❌ Could not reach index server: ${seedersResp.error || 'unknown'}`); return []; } const seeders = seedersResp.data.seeders || []; ``` Registry-provided addresses are probed without cryptographic identity verification: ```javascript const probeResults = await Promise.allSettled( [...uniqueIPs.entries()].map(async ([ip, seeder]) => { const url = `http://[${ip}]:${MEMORY_STORE_PORT}/health`; const health = await probe(url, probeTimeout); let stats = null; if (health.ok) { const statsResp = await probe(`http://[${ip}]:${MEMORY_STORE_PORT}/api/v1/stats`, probeTimeout); if (statsResp.ok) stats = statsResp.data; } return { ip, seeder, health, stats, hasStorage: health.ok, }; }) ); ``` A node passing the health check is generated as enabled: ```javascript configEntry: r.hasStorage ? { id: `filstream-${r.ip.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-')}`, type: 'memory-store', url: `http://[${r.ip}]:${MEMORY_STORE_PORT}`, location: `${region} (${r.ip})`, priority: 10, // discovered nodes get lower priority than manually configured enabled: true, discovered: true, discoveredAt: new Date().toISOString(), seederId: r.seeder.id, } : null, ``` The discovered nodes are written into the active configuration: ```javascript const newNodes = discoveredNodes .filter(n => n.hasStorage && n.configEntry && ...[truncated 3701 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Default all discovered nodes to `enabled: false`. 2. Require explicit user review and approval before a discovered node becomes a replication target. 3. Retrieve discovery information exclusively over HTTPS and validate the registry certificate. 4. Require discovery documents to carry a verifiable signature from a pinned registry key. 5. Give each storage node a cryptographic identity and require proof of private-key possession during discovery and storage requests. 6. Maintain a user-controlled allowlist of approved node identities rather than trusting addresses alone. 7. Authenticate storage requests and bind authorization to a specific node identity and namespace. 8. Display the exact data and metadata exposure involved before enabling a node. 9. Apply separate trust tiers so unverified nodes receive no objects, or only explicitly selected low-sensitivity objects. 10. Detect unexpected node-identity changes even when an IP address remains unchanged. 11. Enforce minimum trusted-replica requirements rather than counting arbitrary successful HTTP acknowledgements as trusted replicas. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/storage.mjs:588
Finding
Ethereum transaction-signing private key is reused as the storage encryption root secret<![CDATA[ ## Vulnerability Details **File Location**: `scripts/storage.mjs:588-600` **Vulnerability Type**: High-value credential reuse and excessive secret-file access **Risk Level**: Medium ### Evidence ```javascript function getKEK() { let secret = process.env.STORAGE_PRIVATE_KEY; if (!secret) { const walletEnv = resolve(SECRETS_DIR, 'eth-wallet.env'); if (existsSync(walletEnv)) { const content = readFileSync(walletEnv, 'utf8'); const match = content.match(/PRIVATE_KEY=([0-9a-fA-Fx]+)/); if (match) secret = match[1]; } } if (!secret) throw new Error('No encryption key. Set STORAGE_PRIVATE_KEY or ensure .secrets/eth-wallet.env exists.'); return deriveKEK(secret); } ``` The key is then used as HKDF input: ```javascript function deriveKEK(secretHex) { const secret = Buffer.from(secretHex.replace(/^0x/, ''), 'hex'); const salt = Buffer.from('cortex-storage-private-v1', 'utf8'); const info = Buffer.from('storage-private-kek', 'utf8'); return Buffer.from(hkdfSync('sha256', secret, salt, info, 32)); } ``` ### Technical Analysis When `STORAGE_PRIVATE_KEY` is absent, the Skill reads `~/.openclaw/workspace/.secrets/eth-wallet.env`, extracts `PRIVATE_KEY`, and uses that Ethereum private key as the root input for storage key derivation. HKDF provides domain-separated derived key material, but it does not eliminate the operational risk of loading and reusing a high-value transaction-signing credential. Encrypted storage does not require access to a wallet key; a dedicated random storage secret provides the necessary cryptographic capability with a smaller privilege scope. This design couples two independent security domains: - Compromise of the wallet key exposes both blockchain assets and stored data. - Code that only needs storage decryption is granted access to a financial signing secret. - Wallet rotation can make historical objects inaccessible unless the old wallet key is retained. - Storage backup requirement ...[truncated 2162 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate a dedicated, cryptographically random 256-bit storage master key. 2. Never automatically fall back to a transaction-signing private key. 3. Store the dedicated key in an operating-system keychain, hardware-backed secret store, or file with restrictive permissions. 4. If wallet-based authorization is desired, use a one-time signed derivation or enrollment protocol that does not require the storage process to read the raw wallet key. 5. Validate key input strictly: - Accept exactly 64 hexadecimal characters after an optional `0x` prefix. - Reject malformed, truncated, empty, or out-of-range values. - Verify that decoding produces exactly 32 bytes. 6. Support explicit key versioning and secure key rotation so historical objects can be migrated without retaining transaction-signing keys indefinitely. 7. Minimize secret lifetime in memory and avoid including secret-derived values in errors or logs. 8. Document backup and recovery procedures for the dedicated storage key. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (16)

Ae1

High
Category
analysis-evasion
Content
node scripts/discover.mjs --update-config
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/discover.mjs --update-config
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/storage.mjs put my-key --data "secret" --ns my-namespace
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/storage.mjs put my-key --data "secret" --ns my-namespace
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/storage.mjs put my-key --data "secret" --ns my-namespace
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/storage.mjs put my-key --data "secret" --ns my-namespace
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/storage.mjs put my-key --data "secret" --ns my-namespace
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises capabilities that require access to environment variables and external networking, but it does not declare any tool scope or permissions boundary in the manifest. This creates a transparency and least-privilege problem: users and hosting platforms cannot easily assess or constrain the skill before execution, increasing the chance of unexpected secret access or outbound communication.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill emphasizes encryption and resilience, but it does not explicitly warn that data is sent to externally discovered nodes and replicated broadly across multiple backends. Even with client-side encryption, this expands the trust boundary, exposes metadata such as access patterns and object presence, and increases the blast radius if discovery is poisoned or a user misunderstands where their data is going.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The configuration explicitly trusts a manual core set of nodes while also enabling automatic discovery and addition of external nodes over unauthenticated plain HTTP. That creates a supply-chain/trust-boundary failure: a malicious or spoofed seeder/index can inject attacker-controlled storage endpoints, causing data exfiltration, traffic interception, metadata leakage, denial of service, or corruption of availability assumptions even if object contents are client-side encrypted.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The manifest describes encrypted multi-node storage and automatic FilStream discovery, but this file implements discovery by importing child_process and executing another script via execSync. Spawning subprocesses is a materially broader capability than basic storage operations and is not necessary for storing, retrieving, or replicating encrypted objects.

Context-Inappropriate Capability

Medium
Confidence
96% confidence
Finding
The skill automatically reads an ETH private key from `~/.openclaw/workspace/.secrets/eth-wallet.env` when `STORAGE_PRIVATE_KEY` is absent, giving the storage tool credential-access behavior beyond ordinary encrypted blob handling. Even though the key is used for derivation rather than exfiltrated in this file, accessing a wallet secret from disk increases blast radius: compromise or misuse of this tool exposes highly sensitive material tied to other financial or agent capabilities.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
Sensitive key material is consumed from environment variables or a secrets file without any user-facing disclosure, prompt, or consent in the command flow. In an agent/tooling environment, silent secret access is dangerous because operators may invoke a storage command expecting data handling only, while the tool also reaches into shared credential storage and derives long-lived encryption keys from wallet material.

Missing User Warnings

Low
Confidence
94% confidence
Finding
The node discovery flow updates local configuration files, but the skill description does not clearly warn users that running discovery mutates local state. Silent configuration changes can redirect future storage operations to different nodes or persist unreviewed network-derived settings, which is risky in a security-sensitive storage skill.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This code writes to config.json, which changes local configuration state, but the updateConfig function itself has no confirmation prompt and no nearby user-facing warning before the write occurs. Although the header usage text mentions auto-updating config.json, the safety disclosure around the actual file modification is minimal for a state-changing operation.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The `discover` command shells out via `execSync` to run another Node script. Although this is part of the implementation, there is no nearby comment, prompt, or user-facing warning that invoking discovery will execute a subprocess.