Back to skill

Security audit

Sensitive Data Masker

Security checks for vulnerabilities and agentic risk

Overview

This skill is intended to mask secrets, but its hook appears to fail open and may let original sensitive messages continue downstream while storing recoverable mappings locally.

Review this skill carefully before installing. Its goal is privacy-protective, but current artifacts indicate it may not actually mask messages before they reach downstream processing, and it stores recoverable secret mappings locally longer than users may expect unless manually cleaned.

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

T09 · Insecure Skill Coding Practices

Error
Location
handler.js:32
Finding
Message-Masking Hook Fails Open Because stderr Is Configured as Ignored<![CDATA[ ## Vulnerability Details **File Location**: `handler.js:32-42` and identical code in `handler.en.js:32-42` **Vulnerability Type**: Fail-open error handling resulting in disclosure of sensitive message content **Risk Level**: High ### Vulnerable Code ```javascript const masker = spawn('python3', [MASKER_SCRIPT, 'mask', content], { stdio: ['pipe', 'pipe', 'ignore'] }); let output = ''; let error = ''; masker.stdout.on('data', (data) => { output += data.toString(); }); masker.stderr.on('data', (data) => { error += data.toString(); }); ``` The surrounding exception handler explicitly permits processing to continue with the original message: ```javascript } catch (error) { console.error('[sensitive-masker] Handler error:', error.message); // Error doesn't affect message processing, continue with original message } ``` ### Technical Analysis Node.js returns `null` for `masker.stderr` when the child process's stderr stream is configured as `"ignore"`. The subsequent call to `masker.stderr.on(...)` therefore throws a `TypeError`. The exception is caught by the outer handler, which neither blocks the message nor substitutes a safe fallback. Consequently, the handler exits before waiting for the Python process and before assigning the masked result to `event.context.content`. This is a deterministic fail-open condition in the security control. It undermines the Skill's primary declared purpose: preventing credentials and personally identifiable information from reaching the downstream LLM API. Passing the message as an element of an argument array does avoid shell interpolation, so this is not a command-injection finding. The issue is the inconsistent child-process stream configuration and unsafe failure policy. ### Attack Path 1. A user or attacker submits a message containing a password, API key, database URL, email address, or other detected sensitive value. 2. The `message:received` hook invokes the Python wrapper. 3. The c ...[truncated 1251 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Configure stderr as an actual pipe when registering a listener: ```javascript const masker = spawn('python3', [MASKER_SCRIPT, 'mask', content], { stdio: ['ignore', 'pipe', 'pipe'] }); ``` Alternatively, remove the stderr listener if stderr must remain ignored. Piping stderr is preferable because it preserves diagnostic information. 2. Handle process startup failures explicitly: ```javascript const exitCode = await new Promise((resolve, reject) => { masker.once('error', reject); masker.once('close', resolve); }); ``` 3. Apply a fail-closed policy for this security boundary. If masking cannot be completed, do not forward the original message. Return an explicit processing error or replace the content with a safe placeholder. 4. Validate the wrapper output before modifying the event: ```javascript const result = JSON.parse(output); if (typeof result.masked !== 'string') { throw new Error('Invalid masker output'); } ``` 5. Add integration tests that: - Submit a message containing a known test credential. - Assert that `event.context.content` contains a mask marker rather than the credential. - Simulate Python startup failure, malformed JSON, and nonzero exit status. - Assert that every failure mode blocks or safely redacts the original content. 6. Apply the same correction to `handler.en.js`, or remove duplicated executable variants to prevent future security fixes from diverging. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
sensitive-masker.py:346
Finding
Expired Sensitive Records Are Not Automatically Deleted Despite Declared Auto-Cleanup<![CDATA[ ## Vulnerability Details **File Location**: `sensitive-masker.py:52-54`, `sensitive-masker.py:346-379`, and `sensitive-masker.py:552-559`; identical behavior exists in `sensitive-masker.en.py` **Vulnerability Type**: Excessive retention of encrypted credentials and personally identifiable information **Risk Level**: Medium ### Vulnerable Code The configuration declares automatic cleanup: ```python DEFAULT_CONFIG = { "enabled": True, "ttl_days": 7, "cache_size": 1000, "auto_cleanup": True, "cleanup_interval_hours": 1, ``` A cleanup function exists, but it is only a callable method: ```python def cleanup_expired(self) -> int: """Clean expired data.""" conn = sqlite3.connect(self.db_path) cursor = conn.cursor() cursor.execute(''' DELETE FROM mappings WHERE expires_at < ? ''', (datetime.now().isoformat(),)) deleted = cursor.rowcount conn.commit() conn.close() # Clean cache now = datetime.now() expired_keys = [ k for k, v in self.cache.items() if v['expires_at'] < now ] for key in expired_keys: del self.cache[key] return deleted ``` The entry point invokes cleanup only when a user manually selects the `cleanup` command: ```python elif cmd == 'stats': show_stats() elif cmd == 'cleanup': cleanup() elif cmd == 'clear': clear_all() ``` No initialization path, periodic scheduler, hook execution path, or interval check invokes `cleanup_expired()` automatically. ### Technical Analysis The database query in `get()` prevents expired values from being restored through the normal API, but it does not remove those values from SQLite. The `expires_at` column therefore acts only as a logical access check until the separate manual cleanup command is run. Although stored values are protected with Fernet encryption, the encryption key and database are both retained under the same local data directory: - Database: `~/.opencl ...[truncated 2309 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Perform rate-limited cleanup during normal initialization or message processing. For example, persist the last cleanup time and call `cleanup_expired()` when `cleanup_interval_hours` has elapsed. 2. Load and enforce the documented configuration rather than defining unused settings. Validate: - `auto_cleanup` - `cleanup_interval_hours` - `ttl_days` - `cache_size` 3. If cleanup must be performed by an external scheduler, install no hidden persistence mechanism. Instead, explicitly document an administrator-configured scheduler and ensure the Skill remains safe when the scheduler is absent. 4. Delete expired rows in a transaction and consider issuing SQLite maintenance appropriate to the threat model. Normal deletion may leave recoverable pages in SQLite freelists or backups. Options include enabling `PRAGMA secure_delete = ON` and periodically running controlled database compaction. 5. Define and enforce backup-retention requirements because deleting the live row does not remove copies already captured in backups. 6. Store the key using an operating-system credential facility where available, rather than colocating it with the encrypted database. At minimum: - Verify restrictive permissions on the data directory, database, and key. - Reject symlinks and unexpected file ownership. - Set the database file to owner-only access after creation. 7. Add tests that insert an already-expired record, execute the automatic cleanup path, and verify physical removal from the mappings table without requiring a manual CLI command. 8. Apply the same changes to `sensitive-masker.en.py`, or consolidate the duplicate implementations into one maintained executable source. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (43)

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The guide instructs users to back up the sensitive mapping file, but that file contains recoverable originals for masked secrets such as passwords, API keys, and connection strings. Backups duplicate the full secret store and often end up in less protected locations, so compromise of the backup would directly expose the underlying sensitive data and defeat the masking design.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The design explicitly restores masked secrets before task execution, re-exposing passwords, tokens, or connection strings to downstream components. Without an explicit user warning, consent model, or strict scoping of where restoration occurs, users may incorrectly assume masking provides end-to-end protection, leading to unintended secret disclosure during tool execution or logging.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The design persists original sensitive values in SQLite and in-memory cache for up to 7 days, creating a concentrated store of recoverable secrets. Even if encryption is mentioned elsewhere, this section does not guarantee encrypted-at-rest storage or user awareness of retention, so compromise of the host, backups, logs, or process memory could expose credentials and personal data.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The design masks secrets before sending content to the LLM, but then restores them before downstream task execution without any explicit consent, warning, or policy boundary. That creates a re-exposure path where sensitive data may be passed into tools, scripts, connectors, or logs, defeating the privacy benefit of masking and potentially leaking credentials during execution.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The sample detector implementation hard-codes `language='zh'`, and the configuration example also sets the Presidio language to `zh`. This appears to impose a specific locale by default without stating that users can choose or override the language, which conflicts with the language/locale policy criteria.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The design explicitly states that original sensitive data will be stored encrypted, but the earlier mapping-store code path writes and caches plaintext originals. In a skill whose core purpose is handling passwords, API keys, and other PII, storing recoverable plaintext in SQLite and memory significantly increases exposure if the local host, backups, logs, or filesystem are accessed.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```bash
# Database file
chmod 600 ~/.openclaw/data/sensitive-masker/mapping.db

# Configuration file
chmod 600 ~/.openclaw/config/sensitive-masker.json
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```bash
# Database file
chmod 600 ~/.openclaw/data/sensitive-masker/mapping.db

# Configuration file
chmod 600 ~/.openclaw/config/sensitive-masker.json
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```bash
# Database file
chmod 600 ~/.openclaw/data/sensitive-masker/mapping.db

# Configuration file
chmod 600 ~/.openclaw/config/sensitive-masker.json
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
```bash
# Database file
chmod 600 ~/.openclaw/data/sensitive-masker/mapping.db

# Configuration file
chmod 600 ~/.openclaw/config/sensitive-masker.json
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The configuration snippet sets `"language": "zh"` as the illustrated operating language while the document elsewhere emphasizes broad language support. Without an explicit opt-in or user-choice mechanism, this is a natural-language locale policy concern.

Session Persistence

Medium
Category
Rogue Agent
Content
return store.get(mask_id)
```

### 2️⃣ Batch Write

```python
def batch_add(self, items: list):
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
return store.get(mask_id)
```

### 2️⃣ Batch Write

```python
def batch_add(self, items: list):
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The README explicitly states that masked user content is sent to an API and that restoration mappings are stored for 7 days, but it does not prominently warn users that sensitive originals remain recoverable locally during that retention window. In a privacy-focused masking skill, this omission can mislead users into assuming data is fully protected or ephemeral, increasing the risk of unintended disclosure from local compromise, backups, or multi-user environments.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The README instructs users to install `zh_core_web_sm`, which implies a Chinese-language processing requirement, but the document does not explain that the skill is limited to Chinese or provide an opt-in or alternative locale choice. This can violate language/locale policy because it imposes a specific locale assumption without justification in the user-facing text.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README explicitly describes sending masked user content to an API while also storing reversible mappings locally for 7 days, but it does not warn users about the privacy, retention, or re-identification implications of that design. Because the masking is reversible and the mapping store becomes a concentration point for secrets, users may unknowingly expose highly sensitive data despite believing it is safely redacted.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
The example implementation hard-codes `language='zh'` in a masking function, which can cause the detector to analyze non-Chinese input with the wrong locale model. In a sensitive-data masking skill, this is security-relevant because missed detections can leave PII, credentials, or secrets unredacted and expose them downstream.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
This example also fixes Presidio analysis to `language='zh'`, creating the same risk of under-detecting entities when messages are in English or mixed-language content. Given this skill is intended for general sensitive-data masking, the context makes the issue more dangerous because users may assume broad protection while the implementation silently narrows coverage.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill explicitly states that original sensitive data is stored locally for 7 days and restored before task execution, but the documentation frames the flow as 'Safe' without a prominent warning about this retention and rehydration behavior. This can mislead users into believing secrets are only masked, when in reality plaintext secrets are persisted and later reintroduced into execution context, increasing exposure if the host, logs, or local storage are compromised.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The documented configuration sets Presidio `language` to `zh`, which indicates a locale-specific default. The file does not clearly offer user opt-in or explain why the skill is constrained to Chinese, creating a potential language/locale policy issue.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The markdown lists `python3 sensitive-masker.py clear` under management commands, but there is no warning that it clears all mappings and can prevent later restoration of masked data. For a destructive operation affecting stored user data, the skill description should explicitly disclose the consequence.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill advertises shell-capable installation/command execution but does not declare an explicit tool scope such as permissions or allowed-tools. That creates an authorization transparency gap: operators cannot easily tell that the skill may invoke shell commands, increasing the chance of unintended command execution during install or runtime.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The feature description emphasizes masking but does not clearly warn that the original sensitive values are retained locally for 7 days. Users may assume data is irreversibly redacted, when in fact recoverable plaintext secrets remain on disk, creating confidentiality and expectation risks if the host is compromised or shared.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The documentation presents masking as making LLM use 'safe' without an equally clear disclosure that values are restored before task execution. This can mislead users into believing downstream actions will operate only on masked data, while the system actually reintroduces secrets, increasing the chance of accidental disclosure or unsafe automated use.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The markdown includes a command to clear all mappings, which would remove restoration data and could permanently prevent recovery of masked values. The documentation does not warn that this action is destructive or irreversible.

Static analysis

Detected: suspicious.dangerous_exec, suspicious.exposed_secret_literal

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
handler.en.js:32

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
handler.js:32

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
CHANNEL-MASKER-GUIDE.md:38