Back to skill

Security audit

MFA WORD

Security checks for vulnerabilities and agentic risk

Overview

This security-gate skill is not malicious, but its advertised protection is weaker than users would reasonably expect and it stores authentication state locally with weak safeguards.

Review before installing. This skill may be useful only as an advisory prompt workflow, not as a real MFA or access-control boundary. Do not rely on it to protect secrets, shell commands, .env files, or deletions unless the surrounding runtime separately enforces those checks. Use strong, unique secret words if testing it, and be aware it writes a local vault and audit log under ~/.openclaw.

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

T09 · Insecure Skill Coding Practices

Error
Location
index.js:23
Finding
Sensitive-resource policy is not enforced by executable code<![CDATA[ ## Vulnerability Details **File Location**: `index.js:23-29` and `index.js:49-64`; related instructions at `SKILL.md:11-17` **Vulnerability Type**: Security control bypass caused by unenforced policy **Risk Level**: High ### Vulnerable Code ```javascript export const initialize_mfa = ({ secret, super_secret, sensitive_list, use_dead_mans_switch = false }) => { const data = { secret_hash: hash(secret), super_hash: hash(super_secret), sensitive_list: sensitive_list || [".env", "password", "config", "sudo"], dead_mans_switch: use_dead_mans_switch }; if (!fs.existsSync(path.dirname(STORE_PATH))) fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true }); fs.writeFileSync(STORE_PATH, JSON.stringify(data)); logEvent("INITIALIZATION", "SUCCESS"); return `✅ MFA Word active. Mode: ${use_dead_mans_switch ? "Dead Man's Switch" : "15-min Window"}`; }; ``` ```javascript export const check_gate_status = () => { if (!fs.existsSync(STORE_PATH)) return { status: "LOCKED" }; const vault = JSON.parse(fs.readFileSync(STORE_PATH)); if (sessionState.isUnlocked && Date.now() < sessionState.expiry) { if (vault.dead_mans_switch) { sessionState.isUnlocked = false; logEvent("AUTO_LOCK", "DEAD_MAN_SWITCH_TRIGGERED"); return { status: "OPEN_ONCE" }; } return { status: "OPEN" }; } sessionState.isUnlocked = false; return { status: "LOCKED" }; }; ``` ### Technical Analysis The configured `sensitive_list` is persisted but never evaluated by any executable authorization mechanism. `check_gate_status` merely reports process-local state; it does not receive an operation, normalize a target path, evaluate whether a resource is sensitive, or intercept filesystem and command-execution tools. The protection therefore depends exclusively on an agent following the prose in `SKILL.md`. It is not an enforceable security boundary. Any code path, tool, ...[truncated 1083 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Enforce authorization in the same executable path that performs sensitive operations. - Wrap or mediate filesystem, shell, deletion, and privileged-command tools rather than relying on prompt instructions. - Pass the requested operation and target into the authorization function. - Canonicalize filesystem paths before policy matching to prevent traversal, symbolic-link, case-sensitivity, and alternate-path bypasses. - Define explicit deny-by-default behavior when the vault is missing, corrupted, or inaccessible. - Bind one-time authorization to a specific operation and target rather than returning a generic `OPEN_ONCE` state. - Treat `SKILL.md` instructions as user-interface guidance only, not as a security control. - Add tests proving that direct access to configured sensitive resources is rejected while the gate is locked. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
index.js:8
Finding
Primary and recovery secrets are stored using unsalted fast SHA-256 hashes<![CDATA[ ## Vulnerability Details **File Location**: `index.js:8`, `index.js:23-25`, `index.js:32-36`, and `index.js:66-69` **Vulnerability Type**: Weak password hashing **Risk Level**: High ### Vulnerable Code ```javascript const hash = (word) => crypto.createHash('sha256').update(word).digest('hex'); ``` ```javascript export const initialize_mfa = ({ secret, super_secret, sensitive_list, use_dead_mans_switch = false }) => { const data = { secret_hash: hash(secret), super_hash: hash(super_secret), sensitive_list: sensitive_list || [".env", "password", "config", "sudo"], dead_mans_switch: use_dead_mans_switch }; ``` ```javascript export const verify_access = ({ word }) => { if (!fs.existsSync(STORE_PATH)) return "❌ MFA not configured."; const vault = JSON.parse(fs.readFileSync(STORE_PATH)); if (hash(word) === vault.secret_hash) { ``` ```javascript export const reset_mfa = ({ super_word, new_secret }) => { const vault = JSON.parse(fs.readFileSync(STORE_PATH)); if (hash(super_word) === vault.super_hash) { ``` ### Technical Analysis The primary secret and emergency reset secret are processed with raw SHA-256 and stored without unique salts. SHA-256 is designed to be computationally fast and is unsuitable for storing user-selected authentication secrets. A “secret word” is likely to have substantially less entropy than a cryptographically random token. An attacker who obtains `~/.openclaw/mfa_vault.json` can test password candidates offline at high speed. The absence of salts also permits precomputed dictionaries and reveals when two stored values correspond to the same secret. Both the normal authentication secret and the more powerful reset secret are exposed to this weakness. ### Attack Path 1. Obtain read access to `~/.openclaw/mfa_vault.json`, such as through another local process, a backup, or permissive file permissions. 2. Extract `secret_hash` and `super_hash`. 3. Generate candidate ...[truncated 864 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Replace raw SHA-256 with a password-specific key derivation function such as Argon2id, scrypt, or bcrypt. - Generate a unique cryptographically random salt for each primary and recovery secret. - Configure memory, iteration, and parallelism costs according to the deployment environment and periodically review them. - Store the algorithm identifier, salt, and cost parameters alongside each derived value. - Compare derived values with `crypto.timingSafeEqual` after validating equal buffer lengths. - Encourage high-entropy passphrases or generated secrets, particularly for the emergency reset credential. - Rehash existing credentials after successful authentication when stored parameters are obsolete. - Invalidate and securely migrate vaults created with the legacy unsalted SHA-256 format. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
index.js:23
Finding
MFA vault is written without explicit restrictive permissions<![CDATA[ ## Vulnerability Details **File Location**: `index.js:23-30` and `index.js:66-71` **Vulnerability Type**: Insecure sensitive-file permissions **Risk Level**: Medium ### Vulnerable Code ```javascript export const initialize_mfa = ({ secret, super_secret, sensitive_list, use_dead_mans_switch = false }) => { const data = { secret_hash: hash(secret), super_hash: hash(super_secret), sensitive_list: sensitive_list || [".env", "password", "config", "sudo"], dead_mans_switch: use_dead_mans_switch }; if (!fs.existsSync(path.dirname(STORE_PATH))) fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true }); fs.writeFileSync(STORE_PATH, JSON.stringify(data)); ``` ```javascript export const reset_mfa = ({ super_word, new_secret }) => { const vault = JSON.parse(fs.readFileSync(STORE_PATH)); if (hash(super_word) === vault.super_hash) { vault.secret_hash = hash(new_secret); fs.writeFileSync(STORE_PATH, JSON.stringify(vault)); logEvent("RESET", "SUCCESS"); return "🔄 Secret word successfully reset."; } ``` ### Technical Analysis The vault contains hashes of both authentication secrets, but `fs.writeFileSync` is called without an explicit restrictive file mode. New-file permissions therefore depend on the process umask. Rewriting an existing file also preserves its existing permissions rather than correcting an insecure mode. The implementation additionally does not verify file ownership or reject symbolic links before reading or writing the vault. The confirmed permission weakness can expose the hashes to other local principals when the environment uses permissive defaults or the vault already has broad permissions. ### Attack Path 1. Run initialization under a permissive umask, or arrange for the vault path to refer to an existing file with permissive permissions. 2. Allow the Skill to write the primary and recovery hashes to that file. 3. Read the vault from another lo ...[truncated 603 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the vault with mode `0o600` and ensure the containing directory is restricted, such as mode `0o700`. - Correct the mode of existing vault files with `chmod` after verifying ownership. - Reject symbolic links and unexpected file types; use safe open flags where supported. - Verify that the vault is owned by the expected user before reading or modifying it. - Update the vault through an atomic replacement in the same protected directory, applying restrictive permissions to the temporary file before rename. - Avoid time-of-check/time-of-use patterns based on `existsSync`; securely open the intended file directly. - Apply similarly restrictive controls to the audit log if its contents are considered sensitive. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
index.js:32
Finding
Authentication and recovery endpoints permit unlimited guessing attempts<![CDATA[ ## Vulnerability Details **File Location**: `index.js:32-45` and `index.js:66-75` **Vulnerability Type**: Missing authentication attempt throttling **Risk Level**: Medium ### Vulnerable Code ```javascript export const verify_access = ({ word }) => { if (!fs.existsSync(STORE_PATH)) return "❌ MFA not configured."; const vault = JSON.parse(fs.readFileSync(STORE_PATH)); if (hash(word) === vault.secret_hash) { sessionState.isUnlocked = true; sessionState.expiry = Date.now() + (15 * 60 * 1000); logEvent("CHALLENGE", "SUCCESS"); return "🔓 Access Granted."; } logEvent("CHALLENGE", "FAILED_ATTEMPT"); return "🚫 Incorrect Secret Word."; }; ``` ```javascript export const reset_mfa = ({ super_word, new_secret }) => { const vault = JSON.parse(fs.readFileSync(STORE_PATH)); if (hash(super_word) === vault.super_hash) { vault.secret_hash = hash(new_secret); fs.writeFileSync(STORE_PATH, JSON.stringify(vault)); logEvent("RESET", "SUCCESS"); return "🔄 Secret word successfully reset."; } logEvent("RESET", "FAILED_CRITICAL"); return "💀 CRITICAL: Super Secret Word incorrect."; }; ``` ### Technical Analysis Neither `verify_access` nor `reset_mfa` limits the number or frequency of failed attempts. Failed attempts are logged, but logging alone does not delay or block further guesses. Because each guess uses a single fast SHA-256 operation, an attacker able to invoke these exported functions can test candidate words rapidly. The recovery endpoint is particularly sensitive because a correct `super_word` immediately replaces the primary secret. ### Attack Path 1. Obtain the ability to invoke the exported `verify_access` or `reset_mfa` function. 2. Build a candidate list of common words, leaked passwords, or likely organization-specific terms. 3. Repeatedly call `verify_access({ word: candidate })` and inspect the distinct success response. 4. Alternatively, r ...[truncated 735 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Enforce per-account or per-vault rate limits for both verification and recovery attempts. - Apply exponential backoff and temporary lockouts after repeated failures. - Persist throttling state securely so restarting the process does not reset all protections. - Use stricter limits for emergency-reset attempts than for ordinary authentication. - Combine throttling with a memory-hard password KDF; rate limiting does not address offline attacks. - Protect exported authentication functions from unrestricted invocation by untrusted callers. - Monitor and alert on repeated failures while preventing audit-log growth from becoming a denial-of-service vector. - Use generic failure responses where practical and require administrative recovery after severe lockout thresholds. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • 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 (9)

Credential Access

High
Category
Privilege Escalation
Content
# Local security vault - DO NOT SHARE
mfa_vault.json
node_modules/
.env
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared behavior says the skill simply challenges for a secret word before sensitive actions, but the implementation reportedly also stores credential material and configuration on disk, maintains unlock state, supports a reset secret, logs activity, and ignores the provided sensitive_list for actual enforcement. This mismatch is dangerous because users and downstream agents may trust the skill as a narrow MFA gate while it actually introduces secret persistence, broadened attack surface, and ineffective protection logic that can be bypassed or misunderstood.

Credential Access

High
Category
Privilege Escalation
Content
const data = {
        secret_hash: hash(secret),
        super_hash: hash(super_secret),
        sensitive_list: sensitive_list || [".env", "password", "config", "sudo"],
        dead_mans_switch: use_dead_mans_switch
    };
    if (!fs.existsSync(path.dirname(STORE_PATH))) fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill exposes code capabilities involving environment access without declaring a restrictive tool scope such as permissions or allowed-tools. In a security-themed skill that mediates access to sensitive resources, undocumented capability access weakens least-privilege guarantees and can let the implementation read or influence sensitive data outside the stated gatekeeping behavior.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
Security-relevant events are written persistently to an audit log without user disclosure, creating an unexpected local record of authentication attempts and resets. Even if the log does not include secrets, it can reveal operational behavior and may aid an attacker with local access.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The skill advertises that it protects access to sensitive files or system commands, but the implementation only tracks an in-memory unlocked flag and never actually intercepts or enforce-gates any file reads or command execution. This creates a misleading security boundary: users or downstream agents may believe sensitive operations are protected when in reality nothing prevents direct access.

Context-Inappropriate Capability

Medium
Confidence
85% confidence
Finding
The additional 'super secret' creates a second long-lived credential and recovery path that is broader than the stated purpose of a simple secret-word challenge. Any extra reset credential increases attack surface and, if disclosed or weakly managed, allows an attacker to rotate the primary secret and take over the gating mechanism.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill persists hashes of the secret and super-secret to disk without any user-facing disclosure, which is risky because users may not expect authentication material to be retained locally. Although the values are hashed, offline guessing remains possible if the file is exposed, especially for weak secrets.

Context-Inappropriate Capability

Low
Confidence
76% confidence
Finding
The manifest frames the skill as a challenge gate for access decisions, but the code creates and maintains a local vault file and audit log in ~/.openclaw. Persistent storage may be a possible implementation choice, but the logging and vault management capability is not disclosed by the stated purpose and is not necessary to understand from the manifest alone.

Static analysis

No suspicious patterns detected.