Back to skill

Security audit

agentrelay

Security checks for vulnerabilities and agentic risk

Overview

AgentRelay is a legitimate shared-file relay, but its current implementation can let relay inputs reach outside the intended storage area and stores verification secrets in plaintext.

Review before installing. Use only with trusted agents and non-sensitive payloads unless it is patched to validate event IDs and pointers, enforce storage-directory containment, use private file permissions, avoid logging or returning secrets, and replace the weak six-character secret with a cryptographic token. Avoid enabling automatic handling from untrusted message text.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
__init__.py:107
Finding
Path Traversal Enables Filesystem Access Outside Relay Storage<![CDATA[ ## Vulnerability Details **File Location**: `__init__.py:74-81`, `__init__.py:107-113`, `__init__.py:247-252`, `__init__.py:309-351`, `__init__.py:590-615`, `run_relay.py:24-34` **Vulnerability Type**: Path traversal and insufficient filesystem boundary validation **Risk Level**: High ### Vulnerable Code ```python def get_event_file(event_id: str) -> Path: """获取事件文件路径""" return STORAGE_PATH / f"{event_id}.json" ``` ```python def resolve_alias(ptr: str, storage_root: Path, alias: str = "s") -> Path: """解析别名路径到完整路径""" if ptr.startswith(f"{alias}/"): return storage_root / ptr[len(alias)+1:] return Path(ptr) ``` ```python file_name = f"{event_id}.json" file_path = STORAGE_PATH / file_name with open(file_path, 'w', encoding='utf-8') as f: json.dump(file_content, f, ensure_ascii=False, indent=2) ``` ```python event_id = parsed["event_id"] ptr = parsed["ptr"] file_path = resolve_alias(ptr, STORAGE_PATH, STORAGE_ALIAS) if not file_path.exists(): raise FileNotFoundError(f"File not found: {file_path}") with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) content = data.get("payload", {}).get("content", {}) secret = data.get("meta", {}).get("secret", "") meta = data.get("meta", {}) sender = meta.get("sender") receiver = meta.get("receiver") if not sender or not receiver: raise ValueError( f"Event {event_id} is missing explicit sender/receiver metadata" ) if meta.get("burn_on_read") or content.get("burn_on_read"): file_path.unlink(missing_ok=True) ``` ```python target_event_id = next_event_id if next_event_id else event_id file_path = STORAGE_PATH / f"{target_event_id}.json" if not file_path.exists(): raise FileNotFoundError(f"File not found: {file_path}") with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) if "payload" not in data: data["payload"] = {} if "content" not in data["payload"]: data["payload"]["content"] = {} data["payload"] ...[truncated 3444 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate every event identifier with a strict allowlist, for example: ```python import re EVENT_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{1,128}$") def validate_event_id(event_id: str) -> str: if not EVENT_ID_PATTERN.fullmatch(event_id): raise ValueError("Invalid event ID") return event_id ``` 2. Reject absolute pointers, empty path segments, parent-directory components, and platform-specific path prefixes. 3. Canonicalize and enforce containment before every read, write, or deletion: ```python def safe_storage_path(relative_name: str) -> Path: root = STORAGE_PATH.resolve() candidate = (root / relative_name).resolve() try: candidate.relative_to(root) except ValueError: raise ValueError("Pointer escapes relay storage") return candidate ``` 4. Apply the same validation consistently to send, receive, CMP, verification, update, and cleanup operations. 5. Require the pointer filename to match the validated message event ID rather than trusting them independently. 6. Reject symbolic links or open files using no-follow semantics where supported. Recheck containment after resolving links. 7. Separate read, update, and delete capabilities. Burn-on-read deletion should only be allowed for files that were created and registered by AgentRelay. 8. Add regression tests for absolute paths, `../` traversal, nested traversal, symbolic links, mismatched pointer/event IDs, and platform-specific separators. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
__init__.py:84
Finding
Completion Verification Uses a Predictable Non-Cryptographic Secret<![CDATA[ ## Vulnerability Details **File Location**: `__init__.py:84-86`, `__init__.py:219-221`, `__init__.py:460-475` **Vulnerability Type**: Weak authentication token generation **Risk Level**: Medium ### Vulnerable Code ```python def generate_secret(length: int = 6) -> str: """生成随机 Secret Code""" return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) ``` ```python if secret is None: secret = generate_secret(6) ``` ```python received_secret = parsed["data"] event_record = get_registry_event(event_id) data = None try: data = read_event_file(event_id) expected_secret = data.get("meta", {}).get("secret", "") except FileNotFoundError: expected_secret = event_record.get("secret", "") if not expected_secret: raise FileNotFoundError( f"No event metadata available for {event_id}; cannot verify CMP" ) verified = bool(expected_secret) and received_secret == expected_secret ``` ### Technical Analysis The six-character verification code is generated with Python's `random` module. This module is based on a deterministic pseudorandom number generator and is explicitly unsuitable for authentication secrets. Although six alphanumeric characters provide a nominal search space of 62 to the sixth power, the effective assurance may be lower if an attacker can infer generator state from other outputs generated by the same process. The code is then checked with a normal string equality operation and treated as evidence that the intended receiver read and completed the event. The mechanism only proves knowledge of the token. It does not authenticate the original sender or intended receiver, bind the payload and pointer to the confirmation, protect message integrity, or prevent replay of a previously observed CMP message. ### Attack Path 1. A relay event is created with a six-character secret generated by `random.choices()`. 2. An attacker observes related pseudorandom outputs, obtains local access to store ...[truncated 1117 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Generate verification tokens with the `secrets` module and at least 128 bits of entropy: ```python import secrets def generate_secret() -> str: return secrets.token_urlsafe(32) ``` 2. Compare secret values using `secrets.compare_digest()` to avoid ordinary equality checks for authentication material. 3. Store only a keyed hash or verifier for the token where feasible, rather than retaining the plaintext token in multiple files. 4. Bind confirmations to the complete event context. A keyed MAC should cover at least: - Event ID - Sender and receiver identities - Pointer or canonical file identity - Payload digest - Creation time - Expiration time - Unique nonce 5. Add expiration and one-time-use enforcement to prevent replay. A successfully consumed confirmation should not remain reusable. 6. Authenticate sender and receiver identities independently of token possession. The completion secret should not be presented as proof of sender authorization. 7. Rate-limit failed verification attempts and log failed attempts without recording the supplied secret. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
__init__.py:237
Finding
Sensitive Relay Payloads and Verification Secrets Are Persisted in Plaintext Without Explicit Private Permissions<![CDATA[ ## Vulnerability Details **File Location**: `__init__.py:18-21`, `__init__.py:48-55`, `__init__.py:237-260`, `__init__.py:416-432` **Vulnerability Type**: Insecure storage and excessive sensitive-data logging **Risk Level**: Medium ### Vulnerable Code ```python def ensure_dirs(): """确保目录存在""" STORAGE_PATH.mkdir(parents=True, exist_ok=True) LOG_PATH.mkdir(parents=True, exist_ok=True) REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) ``` ```python def save_registry(registry: Dict[str, Any]) -> None: """写回持久化 registry。""" ensure_dirs() with open(REGISTRY_PATH, 'w', encoding='utf-8') as f: json.dump(registry, f, ensure_ascii=False, indent=2) ``` ```python file_content = { "meta": { "event_id": event_id, "type": message_type, "secret": secret, "created_at": datetime.now().isoformat(), "sender": sender, "receiver": receiver, "ttl_hours": ttl_hours, }, "payload": { "content": content } } file_name = f"{event_id}.json" file_path = STORAGE_PATH / file_name with open(file_path, 'w', encoding='utf-8') as f: json.dump(file_content, f, ensure_ascii=False, indent=2) upsert_registry_event( event_id, { "event_id": event_id, "type": message_type, "secret": secret, "sender": sender, "receiver": receiver, "ttl_hours": ttl_hours, "ptr": get_file_alias_path(file_path, STORAGE_PATH, STORAGE_ALIAS), "file_path": str(file_path), "file_exists": True, "status": "sent", "created_at": file_content["meta"]["created_at"], }, ) ``` ```python cmp_msg = build_csv("CMP", event_id, "", effective_secret) upsert_registry_event( event_id, { "event_id": event_id, "type": meta.get("type", "REQ"), "secret": effective_secret, "sender": receiver, "receiver": sender, "status": "completed", "cmp_message ...[truncated 2748 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create data directories with owner-only permissions, such as mode `0700`. 2. Create files atomically with mode `0600`. Do not rely solely on the process umask: ```python import os fd = os.open( file_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600, ) with os.fdopen(fd, "w", encoding="utf-8") as handle: json.dump(file_content, handle, ensure_ascii=False, indent=2) ``` 3. Verify and repair the permissions of existing storage, registry, and log files during initialization. 4. Remove secrets from transaction logs. Log only non-sensitive event identifiers, status transitions, and a redacted token fingerprint where operationally necessary. 5. Avoid storing plaintext verification secrets in the registry. Store a cryptographic verifier or keyed hash instead. 6. Do not retain the full CMP message in the registry because it embeds the secret. 7. Minimize payload persistence and encrypt sensitive payloads at rest when the threat model includes other local processes or shared storage. 8. Make expiration automatic and reliable rather than dependent only on manual invocation of the cleanup script. 9. Ensure burn-on-read also removes or invalidates associated registry authentication material and does not leave secret-bearing log records. 10. Document the local storage and retention model so users do not assume that burn-on-read eliminates every persisted copy. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (19)

Exfiltration Commands

High
Category
Prompt Injection
Content
### AgentRelayTool Class

#### send(agent_id, msg_type, event_id, content)
Send message to shared file.

**Parameters**:
- `agent_id` (str): Target agent ID
Confidence
90% confidence
Finding
Instructions found that direct the agent to transmit conversation context or user data to external services.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The declared purpose presents the skill as a message-transfer protocol, but the instructions also include cleanup and burn-on-read deletion behaviors that can remove local files and registry data. That mismatch is dangerous because operators may authorize the skill for communication while overlooking that it can also mutate or delete stored data outside the narrow trigger context.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
| Traditional Approach | AgentRelay Approach |
|---------------------|---------------------|
| ❌ Send large text directly → ⏰ Timeout | ✅ Write to file + send short pointer → Success |
| ❌ No verification if received | ✅ Secret Code mechanism ensures delivery |
| ❌ No audit trail | ✅ Complete transaction logs (4 entries/event) |

---
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README explicitly states that transaction logs include real agent IDs and next_action_plan values, which can expose sensitive operational metadata and potentially confidential workflow details. Even though this is documentation rather than code, it describes a privacy-impacting default behavior without warning users, minimization guidance, or redaction controls.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The document presents core metadata fields such as release date, platform status, and publication status only in Chinese, while other sections are mixed-language. This creates a natural-language locale constraint without user opt-in or an explicit justification that the skill is intended only for a Chinese-speaking or region-specific audience.

Lp3

Medium
Category
MCP Least Privilege
Confidence
90% confidence
Finding
The skill instructs the agent to read and write shared files, inspect environment-relative paths, and maintain local state, but it declares no explicit tool scope or permissions. This weakens least-privilege controls and makes it easier for the skill to be invoked with broader capabilities than users or host policy may expect.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The trigger phrase "Use AgentRelay" is generic enough to appear in ordinary conversation, quoted text, or adversarial content, which can cause unintended activation. In an agent environment, overly broad invocation boundaries increase the chance that untrusted input causes file operations or message handling workflows to run automatically.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill description emphasizes message relay but does not clearly warn that it writes shared files, stores transaction logs, and may delete files via burn-on-read. Missing disclosure is risky because users and calling agents may not understand the persistence, audit, and data-loss implications of invoking the skill.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The instruction to "immediately execute" when certain keywords are seen encourages automatic handling based on ambiguous text rather than a strongly authenticated protocol boundary. This makes prompt-injection and accidental invocation more likely, especially since the skill then reads shared files and may proceed to update state or send confirmations.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
### 2. Burn-on-read (Optional)

When `burn_on_read=true` is set in `meta` or `payload.content`, the file is automatically deleted after reading to protect sensitive data.

## 📁 Data Storage
Confidence
80% confidence
Finding
Automatic deletion via burn-on-read is not inherently malicious, but it is an autonomous destructive action that can remove data without an additional approval step. In this skill's context, that behavior is somewhat justified for confidentiality, but it still creates availability and auditability risks if enabled unexpectedly or triggered by untrusted payload metadata.

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

Medium
Category
Data Flow
Content
def save_registry(registry: Dict[str, Any]) -> None:
    """写回持久化 registry。"""
    ensure_dirs()
    with open(REGISTRY_PATH, 'w', encoding='utf-8') as f:
        json.dump(registry, f, ensure_ascii=False, indent=2)

def upsert_registry_event(event_id: str, event_data: Dict[str, Any]) -> Dict[str, Any]:
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The code persists the relay secret into both the event file and the registry in plaintext. Because the secret is the mechanism later used to validate completion, anyone with local read access to these files can recover it and forge or replay CMP confirmations, undermining the protocol's trust model.

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

Medium
Category
Data Flow
Content
file_name = f"{event_id}.json"
    file_path = STORAGE_PATH / file_name
    
    with open(file_path, 'w', encoding='utf-8') as f:
        json.dump(file_content, f, ensure_ascii=False, indent=2)

    upsert_registry_event(
Confidence
89% confidence
Finding
The event file path is built as STORAGE_PATH / f"{event_id}.json" with no validation of event_id. If event_id contains path traversal sequences such as ../, an attacker may cause writes outside the intended storage directory, potentially overwriting arbitrary files accessible to the process.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The receive operation loads secrets and payloads from disk and returns them wholesale to callers via content, secret, and full_data. In an agent-to-agent relay skill handling potentially sensitive payloads, broad exposure of raw stored data increases the chance of accidental disclosure to logs, downstream tools, or untrusted prompts.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The verification function returns both expected_secret and received_secret to the caller. This leaks the canonical secret even when verification fails, allowing an attacker who can invoke verification or inspect its output to learn the secret and then submit a valid forged CMP.

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

Medium
Category
Data Flow
Content
data["payload"]["content"].update(updates)
    
    # 写回文件
    with open(file_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    upsert_registry_event(
Confidence
92% confidence
Finding
The update path uses next_event_id or event_id directly in STORAGE_PATH / f"{target_event_id}.json" and then overwrites that file. Without sanitizing the identifier, a crafted value can escape the storage directory and modify arbitrary files writable by the process.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The trigger patterns include broad natural-language phrases like "Please use AgentRelay skill to handle" and "Use AgentRelay", which can be embedded in ordinary user or agent messages and unintentionally activate the skill. In a communication/relay skill, accidental activation is more dangerous because it may cause cross-agent message handling, file-pointer processing, or secret-code workflows to start in contexts where they were not intended.

Missing User Warnings

Low
Confidence
84% confidence
Finding
This function rewrites existing event JSON files with updated payload content, which is a file-modifying operation. The code includes internal comments and logging, but no user-facing warning that invoking the tool will alter persisted relay data.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
The top-level docstring is written entirely in Chinese and states a mandatory usage requirement, which imposes a specific language context without any visible opt-in or alternative. Under the policy criteria, forcing a language or locale without user choice is a natural-language policy concern.

Static analysis

No suspicious patterns detected.