Back to skill

Security audit

Agent Mailbox

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local agent mailbox, but it has under-scoped filesystem writes and optional automation that can trigger untrusted tasks or web callbacks.

Review this skill before installing in any shared or sensitive environment. Use only trusted agent identifiers, avoid running the heartbeat example on untrusted mail, do not enable callbacks or cloud sync without allowlists and authentication, and treat mailbox files and exports as plaintext sensitive data. Prefer archive over delete unless permanent removal is intended.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
src/lib/mailbox.ts:64
Finding
Mailbox Path Traversal Through Unvalidated Agent Identifiers<![CDATA[ ## Vulnerability Details **File Location**: `src/lib/mailbox.ts:64-69`, `src/lib/mailbox.ts:109-112` **Vulnerability Type**: Path traversal and unauthorized filesystem write **Risk Level**: High ### Vulnerable Code ```typescript constructor(agentName: string, basePath?: string) { this.agent = agentName; this.basePath = basePath || path.join(process.env.HOME || '/tmp', '.openclaw', 'workspace', 'mailbox'); this.inboxPath = path.join(this.basePath, agentName, 'inbox'); this.sentPath = path.join(this.basePath, agentName, 'sent'); this.archivePath = path.join(this.basePath, agentName, 'archive'); this.logPath = path.join(this.basePath, agentName, 'mail.log'); this.initializePaths(); } ``` ```typescript // Save to recipient's inbox const recipientInboxPath = path.join(this.basePath, options.to, 'inbox'); fs.mkdirSync(recipientInboxPath, { recursive: true }); await this.saveMessage(message, recipientInboxPath); ``` ### Technical Analysis The constructor's `agentName` parameter and the `options.to` recipient value are incorporated directly into filesystem paths without validation or containment checks. Node.js path normalization processes `..` components, so a value such as `../../target` can cause the resulting path to escape the intended mailbox root. The `send()` method then recursively creates the resolved directory and writes a Markdown message into it. The constructor similarly creates inbox, sent, and archive directories based on an unvalidated agent name. No call to `path.resolve()` followed by a mailbox-root comparison is performed. The vulnerable operation remains limited by the operating-system privileges of the Node.js process, but it violates the application's intended mailbox boundary. ### Attack Path 1. An attacker gains access to the CLI or another caller of `Mailbox.send()`. 2. The attacker supplies a traversal-bearing recipient, for example: ```bash openclaw mail send \ --to "../../target" \ --subject ...[truncated 1071 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Enforce a strict identifier format for both sender and recipient names: ```typescript const AGENT_ID = /^[A-Za-z0-9_-]{1,64}$/; function validateAgentId(value: string): string { if (!AGENT_ID.test(value)) { throw new Error('Invalid agent identifier'); } return value; } ``` 2. Resolve every generated path and verify that it remains under the canonical mailbox root: ```typescript function resolveMailboxPath(basePath: string, agent: string, folder: string): string { const root = path.resolve(basePath); const candidate = path.resolve(root, agent, folder); if (!candidate.startsWith(root + path.sep)) { throw new Error('Mailbox path escapes configured root'); } return candidate; } ``` 3. Apply the same validation in the constructor and `send()`, rather than trusting API callers to validate input. 4. Reject absolute paths, `.` and `..` path components, path separators, NUL bytes, and control characters. 5. Run the mailbox process under a dedicated, least-privileged operating-system account. 6. Add tests covering Unix and Windows traversal forms, absolute paths, mixed separators, and encoded traversal inputs. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
examples/agent-heartbeat.ts:28
Finding
Unauthenticated Task Processing With Arbitrary Webhook Requests<![CDATA[ ## Vulnerability Details **File Location**: `examples/agent-heartbeat.ts:28-55`, `examples/agent-heartbeat.ts:124-135` **Vulnerability Type**: Unauthenticated automated action and server-side request forgery **Risk Level**: High ### Vulnerable Code ```typescript // 2. Check if it's a task if (msg.metadata?.task_id) { console.log(`Task ID: ${msg.metadata.task_id}`); try { // 3. Execute the task (your custom logic here) const taskResult = await executeTask(msg.metadata.task_id, msg); // 4. Reply with results await mail.reply( msg.id, `Task complete!\n\nResult:\n${taskResult}`, { status: 'completed' } ); console.log(`✓ Task ${msg.metadata.task_id} completed`); // 5. Optionally call a webhook callback if (msg.metadata?.callback_url) { await callWebhook(msg.metadata.callback_url, { task_id: msg.metadata.task_id, status: 'completed', result: taskResult, }); } } catch (error) { ``` ```typescript async function callWebhook( url: string, payload: Record<string, any> ): Promise<void> { console.log(`[WEBHOOK] Calling ${url}`); try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); ``` ### Technical Analysis The supplied heartbeat example is intended to run periodically through cron. It treats any urgent message containing `metadata.task_id` as an executable task and passes the message to `executeTask()` without authenticating the sender, verifying a signature, checking task authorization, or requiring operator approval. After execution, the code passes the message-controlled `metadata.callback_url` directly to `fetch()`. It does not constrain the protocol or destination and does not block loopback, private, link-local, or cloud metadata addresses. Consequently, an attacker who can place or send a qualifying mailbox message can cause the ag ...[truncated 1924 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require cryptographic message signatures and verify them before processing any task. 2. Maintain an authorization policy mapping trusted sender identities to permitted task types. 3. Require explicit operator approval for new senders, sensitive task classes, and callback destinations. 4. Validate callback URLs using a strict allowlist of expected HTTPS origins. 5. Resolve destination hostnames and reject loopback, private, link-local, multicast, reserved, and cloud metadata address ranges for both IPv4 and IPv6. 6. Revalidate the destination after redirects and either disable redirects or apply the same controls to every redirect target. 7. Apply outbound network restrictions at the operating-system or container layer. 8. Do not return sensitive task results to callbacks by default. Use opaque completion identifiers and retrieve results over an authenticated channel. 9. Add timeouts, response-size limits, concurrency limits, and rate limits to outbound requests. 10. Clearly mark the heartbeat as unsafe example code until authentication and destination validation are implemented. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/lib/mailbox.ts:389
Finding
Sender Spoofing and Frontmatter Injection in Message Files<![CDATA[ ## Vulnerability Details **File Location**: `src/cli/commands.ts:15-17`, `src/lib/mailbox.ts:389-407`, `src/lib/mailbox.ts:417-427` **Vulnerability Type**: Identity spoofing and unsafe structured-data serialization **Risk Level**: Medium ### Vulnerable Code ```typescript // Get agent name from environment or config const agent = process.env.AGENT_NAME || process.env.USER || 'agent'; const mail = new Mailbox(agent); ``` ```typescript private messageToYaml(msg: Message): string { const yaml = `id: ${msg.id} from: ${msg.from} to: ${msg.to} subject: ${msg.subject} priority: ${msg.priority} status: ${msg.status} created_at: ${msg.created_at} ${msg.expires_at ? `expires_at: ${msg.expires_at}` : ''} ${msg.read_at ? `read_at: ${msg.read_at}` : ''} ${Object.keys(msg.metadata || {}).length > 0 ? `metadata: ${JSON.stringify(msg.metadata)}` : ''} --- ${msg.body} ${ msg.responses.length > 0 ? `\n## Responses\n\n${msg.responses.map((r) => `**${r.from}** (${r.created_at}):\n${r.body}`).join('\n\n---\n\n')}` : '' }`; return yaml; } ``` ```typescript const lines = frontmatter.trim().split('\n'); const meta: Record<string, any> = {}; for (const line of lines) { const [key, ...valueParts] = line.split(': '); const value = valueParts.join(': ').trim(); if (key === 'metadata') { meta.metadata = JSON.parse(value); } else { meta[key] = value; } } ``` ### Technical Analysis The CLI derives the claimed sender identity directly from `AGENT_NAME` or `USER`, neither of which constitutes authenticated identity. A local caller capable of setting the environment can therefore claim another agent's name. Message fields are then interpolated into a hand-built, line-oriented frontmatter format without escaping or rejecting newline and control characters. A field containing a newline can introduce additional frontmatter lines. The parser processes every line as a key-value pair and overwrites previously stored values when a duplicate key appears ...[truncated 1654 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not treat environment variables as authenticated identity. Load identity from a protected configuration or key store. 2. Sign every message with a sender-specific private key and verify the signature before displaying or processing the claimed sender. 3. Replace manual serialization and parsing with a maintained JSON or YAML library. 4. Define and enforce a strict runtime schema for all message properties. 5. Reject newlines, carriage returns, NUL bytes, and other control characters in identifiers, sender names, recipient names, priority values, and single-line header fields. 6. Enforce enumerated values for `priority` and `status` at runtime rather than relying only on TypeScript types. 7. Reject duplicate keys during parsing instead of silently accepting the last value. 8. Treat unsigned legacy messages as untrusted and prohibit them from triggering automated actions. 9. Add tests for newline injection, duplicate keys, malformed JSON metadata, spoofed identities, and control-character payloads. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
src/lib/mailbox.ts:468
Finding
Ambiguous Substring Matching Selects Incorrect Messages<![CDATA[ ## Vulnerability Details **File Location**: `src/lib/mailbox.ts:468-475` **Vulnerability Type**: Improper resource identifier validation **Risk Level**: Low ### Vulnerable Code ```typescript private findMessageFile(messageId: string, dir: string): string | null { const files = this.getMessageFiles(dir); for (const file of files) { if (file.includes(messageId)) { return file; } } return null; } ``` ### Technical Analysis Message lookup uses `String.includes()` against the entire file path rather than requiring an exact message identifier. Any substring appearing in a directory name, date prefix, or message ID can produce a match. The method returns the first result from the filesystem enumeration and does not detect ambiguity. It is used by operations that read, mark as read, reply to, archive, and delete messages. Consequently, a partial or broadly matching value can cause an operation to affect a different message than the caller intended. ### Attack Path 1. A mailbox contains multiple files whose names share a date or message-ID prefix. 2. A caller supplies a partial identifier such as `msg-2026` or another common substring. 3. `findMessageFile()` returns the first file containing that substring. 4. The requested operation is applied to that file without confirming its internal `id`. 5. For mutating operations, the unintended message may be marked read, replied to, archived, or deleted. ### Impact Assessment A user or integration can disclose or modify the wrong mailbox entry. Possible effects include: - Reading unintended message content. - Replying to the wrong sender. - Marking the wrong message as read. - Archiving or deleting the wrong message. - Nondeterministic behavior when directory enumeration order changes. Exploitation is limited to messages in the directories searched by the current mailbox instance, but it can cause integrity and confidentiality failures within that mailbox. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate message IDs against the exact generated format before lookup. 2. Parse candidate files and compare their internal identifier using exact equality: ```typescript if (msg && msg.id === messageId) { return file; } ``` 3. Alternatively, maintain an explicit index from exact message IDs to canonical filenames. 4. Reject duplicate matches rather than selecting the first result. 5. Ensure filename construction cannot cause collisions. 6. Add tests proving that partial IDs, date prefixes, empty strings, directory substrings, and ambiguous identifiers are rejected. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (15)

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This guide includes direct `npm publish` and `clawhub publish` commands plus promotional follow-up steps without an explicit warning that they will perform irreversible public release to external services. In an agent-skill context, users may treat documentation as operational instructions and accidentally publish unfinished, sensitive, or internal code, which can expose source, metadata, or package names publicly.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The README emphasizes local storage and manual sync options, but it does not clearly warn that messages are stored as plaintext Markdown/YAML files and that syncing the mailbox directory via Git, rsync, or backups can disclose sensitive message contents and metadata. In this skill's context, mailbox messages may contain task details, coordination data, and potentially operationally sensitive information, so understated documentation can cause users to deploy it with incorrect security assumptions.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documentation states messages stay local by default, but it also describes automatic execution of callbacks derived from message metadata. That means untrusted message content can trigger outbound network activity, contradicting the privacy model and increasing the risk of SSRF-like behavior, data exfiltration, or unintended contact with attacker-controlled endpoints.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The heartbeat feature says it will automatically process urgent messages and execute callbacks, but it does not clearly warn users that this can cause outbound network activity and side effects without review. In an agent mailbox context, messages are inherently untrusted inputs, so auto-executing callback behavior materially increases the risk of abuse.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The cloud sync section provides commands to set a cloud URL and API key, but it does not clearly warn that messages and credentials will be transmitted to an external backend. This omission can lead users to expose private message contents and secrets to third-party infrastructure without fully understanding the security implications.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
The process-urgent command performs mailbox state changes by marking urgent messages as read and archiving expired messages during processing. While it logs that urgent messages are being processed, it does not clearly disclose beforehand that invoking this command will modify message status and archive messages automatically.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The export command writes the entire mailbox, potentially including sensitive message contents and metadata, to an unencrypted JSON file in the current working directory without any warning, access control check, or safer destination handling. In multi-user or shared-agent environments, this can lead to inadvertent data exposure through permissive filesystem locations, backups, logs, or accidental commits.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The delete method irreversibly removes a message file with fs.unlinkSync, but the only disclosure is an internal log entry after deletion. There is no confirmation prompt, user-facing notice, or explicit warning comment/docstring indicating that this operation permanently deletes stored user data.

Missing User Warnings

Medium
Confidence
80% confidence
Finding
The archiveExpired method automatically rewrites message files into the archive and deletes the originals via fs.unlinkSync, affecting stored user data without any user-facing disclosure. Internal logging exists, but it does not satisfy the requirement for a visible warning or documented explanation of this safety-impacting behavior.

Intent-Code Divergence

Low
Confidence
83% confidence
Finding
The security section claims no credentials are transmitted with messages, yet the documented message metadata supports arbitrary fields including callback URLs that can drive external requests. Even if credentials are not embedded in the message body itself, this design can still cause authenticated or sensitive workflow data to be sent externally, making the claim misleading and weakening operator trust.

Missing User Warnings

Low
Confidence
89% confidence
Finding
The commands table includes a delete operation, but the markdown does not indicate whether deletion is permanent, reversible, or how it differs from archiving. For an operation that can remove user data, the skill description should include a brief warning to prevent accidental loss.

Vague Triggers

Low
Confidence
83% confidence
Finding
This is a manifest file, so vague-trigger review applies. The description states the skill is an 'Email system for the agent economy' that can 'Send and receive messages between agents, handlers, and users,' but it does not define when the skill should be invoked, what requests should trigger it, or any exclusions, which can contribute to overly broad matching in systems that use manifest text for routing.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "@types/node": "^20.0.0",
    "typescript": "^5.0.0"
  },
  "repository": {
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"
  },
  "repository": {
    "type": "git",
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Intent-Code Divergence

Low
Confidence
93% confidence
Finding
The docstring for this handler says only 'read - Read a specific message', which implies a retrieval operation. In addition to displaying the message, the function changes state at L150-L153 by calling markRead on unread messages, so the inline documentation understates and contradicts the behavior's side effects.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
examples/agent-heartbeat.ts:17