Back to skill

Security audit

Presidio Pii Scrubber for sensitive info

Security checks for vulnerabilities and agentic risk

Overview

This PII-protection skill has a coherent local anonymization purpose, but it needs review because raw customer data can be sent to environment-controlled endpoints and reversible PII mapping files are handled unsafely.

Review before installing. This skill is not clearly malicious, but it handles sensitive customer data and currently relies on environment variables and caller-supplied session IDs without enough validation. Use only with trusted local Presidio endpoints, avoid untrusted session IDs, run as an unprivileged user, and consider fixing endpoint validation and mapping-file path handling first.

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)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/presidio-scrub.py:6
Finding

Raw PII Can Be Transmitted to an Unrestricted Environment-Controlled Endpoint

Content
View full analysis
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/presidio-scrub.py:27
Finding

Path Traversal in Mapping Creation Enables File Overwrite and Permission Changes

Content
View full analysis
1 else str(int(time.time())) ``` The unvalidated identifier is later incorporated into a filesystem path: ```python # Save mapping os.makedirs(MAPPING_DIR, exist_ok=True) mf = os.path.join(MAPPING_DIR, f"{session_id}.json") with open(mf, "w") as f: json.dump({"session_id": session_id, "created": int(time.time()), "reverse_map": reverse_map, "entity_count": len(entities_fwd), "entity_types": list(type_counters.keys())}, f, indent=2) os.chmod(mf, 0o600) ``` ### Technical Analysis `session_id` is attacker-controlled command-line input. It is concatenated with `.json` and passed to `os.path.join` without character restrictions, canonicalization, or a check that the resulting path remains beneath `MAPPING_DIR`. A value containing `../` components can escape the intended mappings directory. On POSIX systems, an absolute second path component also causes `os.path.join` to discard the base directory. The ordinary `open(..., "w")` operation follows symbolic links and truncates an existing target. The subsequent `os.chmod` then changes the target's permissions to `0600`. Mapping creation occurs only when Presidio detects at least one entity, but an attacker can satisfy that condition by supplying input containing an obvious email address, phone number, or other recognized PII. ### Attack Path 1. The attacker identifies a writable target whose effective path can end in `.json`, or prepares a symbolic link at a traversable destination. 2. The attacker invokes `presidio-scrub.py` with a session ID containing path traversal components or an absolute path. 3. The attacker submits text that is expected to trigger at least one PII detection. 4. The analyzer returns one or more entities, ...[truncated 958 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/presidio-restore.py:14
Finding

Path Traversal in Restore Enables Unauthorized JSON Reads and Deletion

Content
View full analysis
1: text = " ".join(args[1:]) elif not sys.stdin.isatty(): text = sys.stdin.read().strip() else: print(json.dumps({"error": "No input text"})); sys.exit(1) mf = os.path.join(MAPPING_DIR, f"{session_id}.json") if not os.path.exists(mf): print(json.dumps({"error": f"No mapping for session {session_id}", "text": text})); sys.exit(1) with open(mf) as f: mapping = json.load(f) ``` After processing, the selected path is deleted by default: ```python if not keep: try: os.remove(mf) except OSError: pass ``` ### Technical Analysis The restore script accepts a caller-controlled session ID and uses it directly in a filesystem path. It does not restrict path separators, reject absolute paths, canonicalize the result, or verify containment within `MAPPING_DIR`. If the selected file contains valid JSON, the script reads it and interprets its `reverse_map` object. Unless `--keep` is supplied, it then deletes the path. Symbolic links are followed during reading. The deletion operation removes the selected directory entry, while traversal can select files outside the intended mapping directory. The attacker must target a path whose final constructed name ends in `.json`, but that restriction still includes many configuration, state, and application-data files. ### Attack Path 1. The attacker identifies a readable, valid JSON file outside the mappings directory that is addressable through traversal and has the required filename suffix. 2. The attacker invokes `presidio-restore.py` with a session ID such as a relative traversal path that resolves to that file. 3. The attacker provides any input text through an argument or standard input. 4. The script opens and ...[truncated 747 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/presidio-scrub.py:27
Finding

Sensitive Mapping Files Are Created with Predictable Names and Race-Prone Permissions

Content
View full analysis
1 else str(int(time.time())) ``` When no session ID is supplied, the current Unix timestamp is used. The file is then created as follows: ```python # Save mapping os.makedirs(MAPPING_DIR, exist_ok=True) mf = os.path.join(MAPPING_DIR, f"{session_id}.json") with open(mf, "w") as f: json.dump({"session_id": session_id, "created": int(time.time()), "reverse_map": reverse_map, "entity_count": len(entities_fwd), "entity_types": list(type_counters.keys())}, f, indent=2) os.chmod(mf, 0o600) ``` ### Technical Analysis The mapping contains the direct association between anonymized tokens and original PII. It is created with ordinary `open(..., "w")`, which is neither exclusive nor resistant to symbolic links. When the default session ID is used, the filename is based only on the current timestamp and is readily predictable. The requested `0600` permissions are applied only after the JSON content has been written and the file has been closed. Initial permissions therefore depend on the process umask. With an unsafe umask or a shared mapping directory, another local user may have a window in which the file is readable. Because creation is not exclusive, an attacker with write access to the mapping directory can pre-create the expected filename or place a symbolic link there. The write can then overwrite an existing file or follow the link to another target. ### Attack Path 1. A local attacker has access to inspect or modify an inadequately protected `MAPPING_DIR`. 2. The attacker predicts a timestamp-based session filename or observes a caller-selected session ID. 3. The attacker repeatedly checks for the new file to read it before `chmod`, or pre-creates the path as a symbolic link. 4. ...[truncated 706 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Rogue AgentSelf-Modification, Session Persistence
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (18)

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · README.md (reported line 49)May include surrounding context.

md
- Docker running locally (Colima recommended for headless Mac Mini, Docker Desktop also works)
- Python 3 (included with macOS)
- curl (included with macOS)

## Quick Install

External Script Fetching

High
Category
Supply Chain
Confidence
90% confidence
Finding

Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Content

Scanner excerpt · SKILL.md (reported line 11)May include surrounding context.

md
emoji: ""
    requires:
      bins:
        - curl
        - python3
        - docker
    files:

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · README.md (reported line 49)May include surrounding context.

md
- Docker running locally (Colima recommended for headless Mac Mini, Docker Desktop also works)
- Python 3 (included with macOS)
- curl (included with macOS)

## Quick Install

Session Persistence

Medium
Category
Rogue Agent
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.

Content

Scanner excerpt · README.md (reported line 55)May include surrounding context.

md
### 1. Start Presidio containers

Create a docker-compose.yml at ~/.openclaw/presidio/ with analyzer (port 5002:3000) and anonymizer (port 5001:3000). Then run: docker compose up -d

IMPORTANT: Presidio containers listen on port 3000 internally. Map 5002:3000 (analyzer) and 5001:3000 (anonymizer). If you map 5002:5002, you'll get empty replies.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 80)May include surrounding context.

md
- 100% local. Presidio containers run on localhost. No data leaves your machine.
- Fail-closed. If Presidio is down, the skill blocks data queries rather than sending unprotected PII.
- Ephemeral mappings. Token-to-real-value mapping files are created per request, stored with chmod 600, and auto-deleted after restore.
- Vanilla containers. Custom recognizers are passed via API calls, not baked into containers. Pull updated images anytime without losing your config.

## Why Colima over Docker Desktop?

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
80% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 108)May include surrounding context.

md
- 100% local. Presidio containers run on localhost. No data leaves your machine.
- Fail-closed. If Presidio is down, the skill blocks data queries rather than sending unprotected PII.
- Ephemeral mappings. Token-to-real-value mapping files are created per request, stored with chmod 600, and auto-deleted after restore.
- Vanilla containers. Custom recognizers are passed via API calls, not baked into containers. Pull updated images anytime without losing your config.

## Why Colima over Docker Desktop?

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill declares executable capabilities through required binaries and referenced scripts, but it does not define any explicit tool scope such as permissions or allowed-tools. In an agent environment, this creates unnecessary ambiguity about what the skill is allowed to access, increasing the risk of over-broad shell, network, environment, or file operations during use.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

This JSON file repeatedly sets "supported_language": "en" for each recognizer, which enforces a single language/locale policy in natural-language configuration. The file provides no indication that users can choose another language or that the English-only restriction is required for a justified regional or compliance reason.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The manifest describes scrubbing customer data such as names, phones, emails, addresses, credit cards, and vessel names before sending data to AI models. This config additionally classifies a large curated list of South Florida, Houston, and statewide Florida city/place names as LOCATION, which materially broadens redaction beyond the stated examples and can remove ordinary geographic context that is not necessarily customer PII.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The manifest enumerates customer-data categories including names, phones, emails, addresses, credit cards, and vessel names, but this config also detects SEA_COOL_PROJECT identifiers. Project IDs are a distinct business identifier category not mentioned in the description, so the code's effective redaction scope is broader than the skill claims.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
70% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/presidio-health.sh (reported line 8)May include surrounding context.

sh
analyzer_ok=false
anonymizer_ok=false

analyzer_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$ANALYZER_URL/analyze" \
  -X POST -H "Content-Type: application/json" \
  -d '{"text":"health check","language":"en"}' 2>/dev/null)
[ "$analyzer_status" = "200" ] && analyzer_ok=true

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The manifest describes a skill focused on protecting PII by scrubbing customer data before model access, but this file performs the inverse operation by replacing tokens with original sensitive values. Reversible pseudonymization is mentioned in the manifest, yet the restoration capability materially expands behavior from protection/scrubbing into re-exposure of PII.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

This code sends the input text to HTTP endpoints via http_post for analysis, and the script's only docstring describes anonymization and mapping storage, not that raw input is transmitted to separate services. For a code file handling sensitive data, this is a safety-relevant network operation lacking visible user disclosure such as a prompt, log message, or warning comment.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script stores a reversible map from pseudonymous tokens back to original PII on disk, creating a concentrated secret that defeats anonymization if accessed by another local user, process, backup system, or compromised account. Although the file mode is set to 0600, the design still materially increases exposure because the mapping persists beyond the immediate operation and is written without any consent, retention, or encryption controls.

Content

No source excerpt is available for this finding.

Tainted flow: 'mf' from os.environ.get (line 98, credential/environment) → open (file write)

Medium
Category
Data Flow
Confidence
93% confidence
Finding

The file path used for the reversible PII mapping is derived from an environment-controlled base directory and an unsanitized session_id taken from argv. An attacker who can influence either value can cause path traversal or arbitrary file overwrite, which is especially sensitive here because the file contains reversible mappings from anonymized tokens back to original PII.

Content

Scanner excerpt · scripts/presidio-scrub.py (reported line 99)May include surrounding context.

python
# Save mapping
    os.makedirs(MAPPING_DIR, exist_ok=True)
    mf = os.path.join(MAPPING_DIR, f"{session_id}.json")
    with open(mf, "w") as f:
        json.dump({"session_id": session_id, "created": int(time.time()), "reverse_map": reverse_map, "entity_count": len(entities_fwd), "entity_types": list(type_counters.keys())}, f, indent=2)
    os.chmod(mf, 0o600)

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The HTML comment includes the statement "I'm praying for you without your consent," which is natural-language content embedded in the skill file and directed at users or maintainers. This introduces an unsolicited personal/religious message that is not justified by the skill's function and can conflict with organizational expectations for neutral, consent-respecting communication.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

This code performs outbound HTTP POST requests to analyzer and anonymizer services, which is a safety-relevant network operation for a code file. The script provides no confirmation prompt, visible notice, or explanatory comment/docstring about contacting external/local services, so users running it may not realize it transmits request data over HTTP.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
93% confidence
Finding

The request body explicitly sets "language":"en", which imposes a specific language choice in natural-language behavior. There is no indication here that the skill offers language selection or that the English-only constraint is documented as intentional and justified.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.