Back to skill

Security audit

AIEOS (AI Entity Object Specification)

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent persona-management purpose, but it needs Review because it can persist imported file or URL content into agent identity and behavior files without strong validation or sanitization.

Install only if you are comfortable with a skill that can change persistent agent identity files. Apply schemas only from sources you trust, review the dry-run output before using --apply, avoid arbitrary URLs, and do not publish generated bio pages from untrusted persona data without sanitizing the HTML first.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • 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)

T02 · Agent Memory Poisoning

Error
Location
scripts/aieos_tool.py:79
Finding
Persistent Agent Instruction Injection Through Untrusted Persona Schemas<![CDATA[ ## Vulnerability Details **File Location**: `scripts/aieos_tool.py:79-84, 99-127, 200-220, 246-255, 524-534` **Vulnerability Type**: Persistent instruction injection and agent memory poisoning **Risk Level**: High ### Vulnerable Code ```python # --- Step 1: Save the full AIEOS schema to entity.json --- PERSONA_DATA_PATH.parent.mkdir(parents=True, exist_ok=True) if apply_changes: with open(PERSONA_DATA_PATH, 'w', encoding='utf-8') as f: json.dump(schema, f, indent=2, ensure_ascii=False) print(f"Updated full AIEOS persona data at {PERSONA_DATA_PATH}") else: changes[str(PERSONA_DATA_PATH)] = json.dumps(schema, indent=2, ensure_ascii=False) ``` Attacker-controlled schema values are inserted into identity content without normalization or separation from instructions: ```python name = names.get('nickname') or names.get('first') if name: identity_updates.append(f"- **Name:** {name}") if names.get('first') or names.get('middle') or names.get('last') or names.get('nickname'): identity_updates.append(f"\n### Names") if names.get('first'): identity_updates.append(f"- **First:** {names['first']}") if names.get('middle'): identity_updates.append(f"- **Middle:** {names['middle']}") if names.get('last'): identity_updates.append(f"- **Last:** {names['last']}") if names.get('nickname'): identity_updates.append(f"- **Nickname:** {names['nickname']}") ``` The same issue affects behavioral fields written into `SOUL.md`: ```python if core_values or moral_compass.get('alignment') or neural_matrix: soul_updates.append("\n## Core Truths\n") if moral_compass.get('alignment'): soul_updates.append(f"**Moral Alignment:** {moral_compass['alignment']}.\n") if core_values: for value in core_values: soul_updates.append(f"**{value}.**") ``` ```python if text_style.get('style_descriptors'): vibe_description_parts.extend(text_style['style_descriptors']) ``` The generated content is then pers ...[truncated 3634 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate imported documents against a strict JSON Schema rather than checking only top-level keys. 2. Enforce exact nested types, numeric ranges, maximum array lengths, and conservative string-length limits. 3. Treat imported persona values as untrusted data and never concatenate them directly into agent instruction documents. 4. Keep imported content in a structured data file and render it only in a clearly delimited, non-instructional data section. 5. Reject or escape Markdown control syntax where values must be written to Markdown. 6. Detect and reject instruction-like content in descriptive fields, including role directives, tool-use requests, and attempts to override existing constraints. 7. Require explicit per-field review and confirmation before changing `SOUL.md` or `IDENTITY.md`. 8. Preserve immutable platform safety rules outside persona-controlled files. 9. Create backups and show a semantic diff before replacing existing identity files. 10. Record the source and integrity hash of imported schemas so administrators can audit or roll back changes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/aieos_tool.py:380
Finding
Stored HTML Injection in Generated Biography Pages<![CDATA[ ## Vulnerability Details **File Location**: `scripts/aieos_tool.py:380-384, 431-468, 477` **Vulnerability Type**: Stored HTML injection and cross-site scripting **Risk Level**: Medium ### Vulnerable Code Persona-controlled values are inserted into HTML attributes without escaping: ```python profile_image_html = f"<img src=\"{portrait_url}\" alt=\"{name}\">\n" if portrait_url else "" ``` They are also inserted directly into HTML element content: ```python html_content_about_me = f""" {profile_image_html} <h1>{name}</h1> <p>{creature_type} | {vibe}</p> </div> <div class="section"> <h2>About Me</h2> <p><strong>Full Name:</strong> {full_name}</p> """ if bio.get('birthday'): html_content_about_me += f" <p><strong>Birthday:</strong> {bio['birthday']}</p>\n" if bio.get('gender'): html_content_about_me += f" <p><strong>Gender:</strong> {bio['gender']}</p>\n" if origin.get('nationality'): birthplace_info = f" ({origin.get('birthplace', {}).get('city', '')})" if origin.get('birthplace', {}).get('city') else "" html_content_about_me += f" <p><strong>Origin:</strong> {origin['nationality']}{birthplace_info}</p>\n" if residence.get('current_city'): dwelling_info = f" ({residence.get('dwelling_type', '')})" if residence.get('dwelling_type') else "" html_content_about_me += f" <p><strong>Residence:</strong> {residence['current_city']}, {residence.get('current_country', '')}{dwelling_info}</p>\n" ``` Array values are rendered as raw list-item markup: ```python if moral_compass.get('core_values'): html_content_truths_start += f" <h3>Core Values:</h3><ul>" html_content_truths_start += ''.join([f"<li>{v}</li>" for v in moral_compass.get('core_values', [])]) html_content_truths_start += f"</ul>\n" ``` Image values are also used as raw attribute values: ```python if portrait_url: image_gallery_html += f ...[truncated 2413 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply `html.escape(value, quote=True)` to every dynamic value before inserting it into HTML. 2. Use an auto-escaping template engine instead of assembling HTML with f-strings. 3. Validate image URLs with a URL parser and permit only explicitly supported schemes, preferably `https`. 4. Reject credentials, control characters, malformed hosts, and unexpected schemes in image URLs. 5. Consider proxying or locally hosting approved images rather than embedding arbitrary remote URLs. 6. Add a restrictive Content Security Policy, for example disallowing scripts and limiting images to approved origins. 7. Add automated tests using payloads in text and attribute contexts, including quotes, angle brackets, event handlers, and closing tags. 8. Keep escaping context-specific: HTML text escaping is required for element content, while quoted attribute escaping and URL validation are both required for URL attributes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/aieos_tool.py:33
Finding
Unrestricted Remote Schema Fetching Enables Server-Side Request Forgery and Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/aieos_tool.py:33-43` **Vulnerability Type**: Server-side request forgery and unbounded network response handling **Risk Level**: Medium ### Vulnerable Code ```python def load_schema(source: str) -> Dict[str, Any]: """Load AIEOS schema from URL or file.""" if source.startswith(('http://', 'https://')): # Load from URL print(f"Loading schema from URL: {source}") try: with urllib.request.urlopen(source) as response: content = response.read().decode('utf-8') return json.loads(content) except Exception as e: raise ValueError(f"Failed to load schema from URL: {e}") ``` ### Technical Analysis The command accepts an arbitrary `http://` or `https://` URL and fetches it from the host running the skill. There is no destination allowlist, IP-address restriction, explicit timeout, maximum response size, or redirect-target revalidation. As a result, the process can be induced to send requests to network locations that are reachable from the agent host but not necessarily reachable by the external attacker. This includes loopback services and private or link-local network addresses. `response.read()` reads the entire response into memory before parsing it. A large response can therefore consume excessive memory, while a slow endpoint can tie up execution because no explicit timeout is supplied. Although JSON parsing limits practical data extraction to responses that parse as JSON, internal JSON APIs may still be accessed. The `load` command also prints successfully parsed responses, which can expose returned internal data to the invoking user or surrounding logs. ### Attack Path 1. An attacker controls or influences the value passed to `--source`. 2. The attacker supplies a URL pointing to an internal service, loopback address, private network host, link-local endpoint, or an external URL that redirects to such ...[truncated 1196 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable remote schema loading by default and require an explicit opt-in option. 2. Prefer a strict allowlist of trusted schema hosts. 3. Require HTTPS for remote sources unless a specific local-development mode is enabled. 4. Resolve the destination hostname and reject loopback, private, link-local, multicast, reserved, and unspecified IP ranges. 5. Revalidate every redirect destination and enforce a small redirect limit. 6. Set explicit connection and read timeouts. 7. Stream the response in bounded chunks and reject bodies above a conservative maximum size. 8. Validate the response `Content-Type` and schema structure before further processing. 9. Avoid printing full remote documents where they may contain sensitive information. 10. Where possible, download schemas through a network-isolated service with narrowly scoped outbound access. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (11)

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill instructs users to run a Python tool that can read local files, write to workspace files, access environment-derived paths, and fetch schemas from URLs, but the manifest declares no explicit tool scope or permissions. This creates an under-specified trust boundary: consumers may install or invoke the skill without realizing it has filesystem and network effects, increasing the risk of unintended data exposure, remote content ingestion, or unauthorized modification of identity files.

Description-Behavior Mismatch

Medium
Confidence
87% confidence
Finding
The tool fetches schemas from arbitrary HTTP/HTTPS URLs, which expands the trust boundary to remote untrusted content. In this skill, remote content is later used to generate local identity files and HTML, so network retrieval meaningfully increases exposure to malicious schemas, tracking, or unexpected outbound requests.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
When `apply_changes` is enabled, the script writes directly to `entity.json` and later rewrites `IDENTITY.md` and `SOUL.md`, which can alter user-managed identity data. Although the CLI supports a dry run by default, there is no interactive confirmation or strong warning at the write points before these files are overwritten.

Tainted flow: 'PERSONA_DATA_PATH' from os.getenv (line 28, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
# --- Step 1: Save the full AIEOS schema to entity.json ---
    PERSONA_DATA_PATH.parent.mkdir(parents=True, exist_ok=True)
    if apply_changes:
        with open(PERSONA_DATA_PATH, 'w', encoding='utf-8') as f:
            json.dump(schema, f, indent=2, ensure_ascii=False)
        print(f"Updated full AIEOS persona data at {PERSONA_DATA_PATH}")
    else:
Confidence
88% confidence
Finding
The write target is derived from OPENCLAW_WORKSPACE, an environment variable that is trusted without normalization or containment checks. If an attacker can influence the environment in which this tool runs, they can redirect persona data writes to arbitrary filesystem locations, potentially overwriting sensitive files the process can access.

Tainted flow: 'IDENTITY_PATH' from os.getenv (line 26, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
# Actually apply changes if requested
    if apply_changes:
        if IDENTITY_PATH.exists():
            with open(IDENTITY_PATH, 'w', encoding='utf-8') as f:
                f.write(identity_content)
            print(f"Updated {IDENTITY_PATH}")
Confidence
86% confidence
Finding
IDENTITY_PATH is built from an environment-controlled workspace path and then opened for writing with no path safety checks. In a hostile execution environment, this enables arbitrary file overwrite within the permissions of the running user, and the file content is attacker-influenced through the supplied schema.

Tainted flow: 'SOUL_PATH' from os.getenv (line 25, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
print(f"Updated {IDENTITY_PATH}")
        
        if SOUL_PATH.exists():
            with open(SOUL_PATH, 'w', encoding='utf-8') as f:
                f.write(soul_content)
            print(f"Updated {SOUL_PATH}")
Confidence
86% confidence
Finding
SOUL_PATH inherits the same trust boundary issue: it comes from an environment-derived base path and is written without restriction checks. This creates an arbitrary file overwrite primitive if the environment is attacker-controlled or if the workspace contains malicious symlinks.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
The bio page generator interpolates untrusted schema fields directly into HTML attributes and element bodies without escaping. If the generated page is opened in a browser, attacker-controlled values such as name, style descriptors, or image URLs can trigger stored XSS or load attacker-controlled resources, turning persona data into executable web content.

Natural-Language Policy Violations

Low
Confidence
77% confidence
Finding
The `accent.region` and `accent.strength` fields define a regional accent profile, which can be used to force a locale-specific speaking style. Because the schema provides no accompanying language or locale choice requirement, it may enable downstream skills to impose a specific locale presentation without user opt-in.

Missing User Warnings

Low
Confidence
85% confidence
Finding
The export function writes schema data directly to the user-supplied `output_path` with no confirmation if the destination already exists. This is a file-modifying operation that may overwrite existing content without a user-facing warning beyond a generic status message.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The generated page sets `<html lang="en">`, which forces an English locale regardless of the user's preferences or the persona data. This is a natural-language locale constraint without opt-in or a documented reason for being English-only.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The bio page generator writes HTML to the specified `output_path` without checking for an existing file or asking the user to confirm replacement. This can silently overwrite prior output or other user data at that path.

Static analysis

No suspicious patterns detected.