Back to skill

Security audit

OmniPermission (Human-in-the-Loop)

Security checks for vulnerabilities and agentic risk

Overview

OmniPermission has a coherent human-approval purpose, but its security-sensitive implementation exposes its secret key and can fail open when approval policy storage is malformed.

Review this before installing in a sensitive environment. Use isolated mode so agents cannot run the OpenClaw CLI, avoid putting highly sensitive tools or prompts through the mobile approval channel unless you accept the external data flow, and rotate the OmniPersona key if it may have been displayed with the status command. The plugin should mask/store secrets more safely and fail closed on invalid policy before being trusted as a security boundary.

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

T09 · Insecure Skill Coding Practices

Error
Location
src/cli.ts:13
Finding
Secret Key Stored in Plaintext and Exposed Through CLI Output## Vulnerability Details **File Location**: `src/cli.ts:13-33`; `src/storage.ts:33-37` **Vulnerability Type**: Plaintext credential storage and disclosure **Risk Level**: High ### Vulnerable Code `src/storage.ts:33-37`: ```ts export const saveKey = async (api: OpenClawPluginApi, key: string) => { const keyPath = getKeyPath(api); await fs.mkdir(path.dirname(keyPath), { recursive: true }); await fs.writeFile(keyPath, key, "utf-8"); }; ``` `src/cli.ts:13-33`: ```ts omni .command("status") .description("Show current Secret Key and blacklisted skills") .action(async () => { const keyPath = getKeyPath(api); let keyContent = "❌ NO SECRET KEY SAVED"; try { keyContent = await fs.readFile(keyPath, "utf-8"); } catch (e) { // File doesn't exist yet } const blacklist = await getInterceptedTools(api); console.log("\n" + "=".repeat(50)); console.log("📂 OMNIPERMISSION CONFIGURATION"); console.log("-".repeat(50)); console.log(`🔑 SECRET KEY:\n${keyContent.trim() || "Empty"}`); console.log("-".repeat(50)); console.log( `🚫 BLACKLISTED SKILLS: ${blacklist.length > 0 ? blacklist.join(", ") : "None (Pass-through mode)"}`, ); ``` ### Technical Analysis The OmniPersona authentication secret is written directly to `omni_key.txt` without encryption or an explicitly restrictive file mode. The effective permissions consequently depend on the process umask and surrounding state-directory permissions. More critically, the `status` command reads the credential and prints its complete value to standard output. This creates additional disclosure channels, including terminal capture, automated logs, support bundles, agent transcripts, and command-execution interfaces exposed to less-trusted users or agents. The project documentation explicitly discusses deployments where an agent can access the OpenClaw CLI. In such a deploy ...[truncated 1322 chars]
Remediation
## Remediation Suggestions 1. Remove the full secret from the `status` command. Display only a masked value or short non-sensitive fingerprint, such as the final four characters. 2. Store the credential in an operating-system credential manager or secret-management service where available. 3. If file storage is unavoidable, create the file with mode `0o600` and verify or repair the permissions of existing files: ```ts await fs.writeFile(keyPath, key, { encoding: "utf-8", mode: 0o600, }); await fs.chmod(keyPath, 0o600); ``` 4. Ensure the containing state directory is accessible only to the OpenClaw service account. 5. Prevent secrets from appearing in application logs, CLI diagnostics, error messages, support bundles, and agent-visible command output. 6. Rotate credentials that may already have been exposed through `omnipermission status`.

T09 · Insecure Skill Coding Practices

Error
Location
src/storage.ts:39
Finding
Malformed Blacklist Configuration Silently Disables Approval Enforcement## Vulnerability Details **File Location**: `src/storage.ts:39-45`; `src/hooks.ts:7-11` **Vulnerability Type**: Fail-open security configuration handling **Risk Level**: High ### Vulnerable Code `src/storage.ts:39-45`: ```ts export const getInterceptedTools = async (api: OpenClawPluginApi): Promise<string[]> => { try { const data = await fs.readFile(getToolsPath(api), "utf-8"); return JSON.parse(data); } catch { return []; } }; ``` `src/hooks.ts:7-11`: ```ts // 1. Blacklist Check const interceptedTools = await getInterceptedTools(api); if (!interceptedTools.includes(event.toolName)) { return; } ``` ### Technical Analysis `getInterceptedTools()` treats every read or parsing failure as an empty blacklist. The hook interprets an empty list as pass-through mode, so no tool requires approval. This behavior conflates a deliberately empty configuration with corrupted JSON, interrupted writes, permission errors, filesystem failures, and malicious modification. The parsed JSON is also not validated to ensure it is an array containing only strings. As a result, the plugin fails open when its security policy cannot be reliably loaded. Because the blacklist determines whether the mobile approval hook runs, silent fallback to an empty array undermines the plugin's primary security function. ### Attack Path 1. An attacker with write access to the OpenClaw state directory modifies `intercepted_tools.json` so that it contains malformed JSON. 2. Alternatively, the file becomes unreadable or corrupted because of a filesystem or interrupted-write error. 3. `JSON.parse()` or `fs.readFile()` throws an exception. 4. The catch block silently returns `[]`. 5. For every tool call, `interceptedTools.includes(event.toolName)` evaluates to false. 6. The hook returns without requesting mobile approval, allowing the tool call to continue. ### Impact Assessment Successful exploitation disa ...[truncated 546 chars]
Remediation
## Remediation Suggestions 1. Distinguish a genuinely absent initial configuration from malformed, unreadable, or invalid existing configuration. 2. Validate parsed data before use: ```ts const parsed: unknown = JSON.parse(data); if ( !Array.isArray(parsed) || !parsed.every((value): value is string => typeof value === "string") ) { throw new Error("Invalid intercepted-tools configuration"); } return parsed; ``` 3. Fail closed when an existing policy cannot be read or validated. The hook should block tool execution and return a clear configuration-error reason rather than interpreting the failure as pass-through mode. 4. Emit a prominent security log when policy loading fails; do not use an empty catch block. 5. Write policy changes atomically by writing a temporary file, synchronizing it where appropriate, and renaming it over the destination. 6. Apply restrictive permissions to the policy file and its containing directory. 7. Consider maintaining a last-known-good validated policy so transient storage failures do not silently remove enforcement.

T09 · Insecure Skill Coding Practices

Warning
Location
src/utils.ts:41
Finding
Unbounded Approval Polling Allows Indefinite Tool-Call Blocking## Vulnerability Details **File Location**: `src/utils.ts:41-79` **Vulnerability Type**: Unbounded network wait and polling loop **Risk Level**: Medium ### Vulnerable Code ```ts const response = await fetch(baseUrl, { method: "POST", headers: headers, body: JSON.stringify(requestBody), }); if (response.status !== 201) { const errorText = await response.text(); api.logger.error(`[omnipermission] ❌ API Error (${response.status}): ${errorText}`); return { approved: false, reason: `POA creation failed (Status ${response.status})` }; } const { id: poaId } = await response.json(); api.logger.info(`[omnipermission] ✅ POA created (ID: ${poaId}). Waiting for OmniPersona approval...`); // Polling Loop while (true) { await new Promise((resolve) => setTimeout(resolve, 1000)); const pollResponse = await fetch(`${baseUrl}/${poaId}`, { method: "GET", headers: headers, }); if (!pollResponse.ok) continue; const pollData = await pollResponse.json(); if (pollData.status === "APPROVED") { api.logger.info(`[omnipermission] 👍 Approved: ${toolName}`); return { approved: true }; } if (pollData.status === "REJECTED") { api.logger.warn(`[omnipermission] 🛑 Rejected by user via OmniPersona.`); return { approved: false, reason: "User rejected the action on OmniPersona." }; } } ``` ### Technical Analysis The initial `POST` and subsequent polling `GET` operations do not use an `AbortSignal` or request timeout. The polling loop is unconditional and has no overall deadline, maximum attempt count, or terminal handling for unexpected statuses. Non-successful polling responses are ignored with `continue`, while successful responses whose status is neither `APPROVED` nor `REJECTED` also cause indefinite polling. A stalled connection can therefore hold a single fetch indefinitely, and a backend that continuously returns pending, malformed, or error respon ...[truncated 1226 chars]
Remediation
## Remediation Suggestions 1. Apply a short per-request timeout to every `fetch` using `AbortController` or `AbortSignal.timeout()`. 2. Enforce an overall approval deadline independent of individual request timeouts. 3. Limit the number of polling attempts and use capped exponential backoff with jitter. 4. Treat persistent HTTP failures, malformed JSON, missing request identifiers, and unknown terminal statuses as explicit fail-closed errors. 5. On timeout or backend failure, return a blocked result with a clear reason and release timers and network resources. 6. Consider allowing operators to configure the approval deadline within safe minimum and maximum bounds. 7. Add concurrency limits so repeated approval requests cannot exhaust gateway resources.
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
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (13)

Missing User Warnings

High
Confidence
98% confidence
Finding
The extension guidance explicitly encourages sending richer context such as the agent's internal reasoning or project identifiers to a phone, without a strong privacy and secrecy warning. This can leak sensitive chain-of-thought, secrets, customer data, repository identifiers, or internal business context to an external mobile service, significantly increasing confidentiality risk.

Missing User Warnings

High
Confidence
99% confidence
Finding
Displaying the saved secret key in cleartext is a direct secret-handling flaw. Even if access requires local CLI use, credentials printed to stdout are commonly captured by terminal logging, demos, screenshots, CI wrappers, remote shells, or support transcripts, which can lead to compromise of the associated backend or mobile approval channel.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README explicitly states that the agent's intent and parameters are sent to a mobile app, but the finding indicates it does not clearly warn users that this data is transmitted to an external app/backend and may contain sensitive content. This can lead to unintentional disclosure of secrets, personal data, prompts, or operational details whenever gated tool calls are intercepted.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill documents network-dependent behavior through a mobile app/service integration but does not declare an explicit tool scope such as permissions or allowed-tools. That omission weakens transparency and enforcement, making it harder for users and platform controls to understand that the skill may rely on network-capable operations and external communication.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill tells users to install a mobile app and use a secret key, but it does not clearly warn that approval notifications may send tool-call metadata to a third-party service. Users may unknowingly expose command names, file paths, prompts, or other sensitive operational details outside their local environment.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The provided skill context describes this skill as a human-in-the-loop framework for intercepting tool calls via the OmniPersona mobile app, but the manifest description says only 'Activity tracking for Gateway hooks.' That narrows and changes the apparent purpose from approval/interception workflow to passive tracking, creating a semantic mismatch in the skill's declared behavior.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
These commands directly change local plugin policy and agent configuration, including allowing the omnipermission plugin and changing the default model, without any inline warning about the security implications. In a human-in-the-loop permission framework, silently broadening plugin permissions is security-relevant because it can enable interception or control paths the operator may not fully understand.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The omnipermission commands include setting keys, blacklisting tools, clearing blacklists, and enabling dev mode, all of which can materially alter security posture or enforcement behavior. Because this file is a convenience command list with no disclosure of consequences, a developer may invoke high-impact commands that weaken protections or change authorization behavior without realizing the risk.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The status command reads the stored secret key from disk and prints it verbatim to stdout. This creates unnecessary credential exposure through terminal history, screen recording, logs, shoulder surfing, or shared shell sessions, and the feature is not required for normal status reporting of an approval/interception tool.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The set-key flow uses readline.question, which echoes the secret as the user types. This exposes the credential to anyone viewing the terminal and may also leak it into recordings or shared-session transcripts; in a human-in-the-loop security tool, mishandling the primary secret undermines the trust model rather than reducing risk.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The code persists an OmniPersona UUID key directly to disk in plaintext using fs.writeFile without any encryption, access-control hardening, or user disclosure. If the local state directory is readable by other local users, malware, backups, or logs, the key can be recovered and used to impersonate or abuse the integration.

Unverifiable Dependency: openclaw has 16 known advisory(ies) (CVE-2026-53846 (OpenClaw: Workspace .env npm_execpath could influence bundled runtime dependency); CVE-2026-32064 (OpenClaw's andbox browser noVNC observer lacked VNC authentication); CVE-2026-32006 (OpenClaw has a BlueBubbles group allowlist mismatch via DM pairing-store fallbac) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
84% confidence
Finding
The manifest uses a workspace reference for the devDependency `openclaw` (`workspace:*`) rather than a pinned, auditable version. That makes it impossible to determine from this file alone whether the dependency includes known vulnerable releases, and in a security-sensitive skill that intercepts high-risk tool calls, a compromised or vulnerable framework dependency could undermine the trust boundary or introduce build/runtime risk.

Intent-Code Divergence

Low
Confidence
90% confidence
Finding
The command description claims it shows only the current secret key and blacklisted skills, but in practice it exposes the full secret key. This mismatch increases the chance that users invoke the command expecting harmless status output and unintentionally disclose a sensitive credential on-screen or into captured logs.

Static analysis

No suspicious patterns detected.