Back to skill

Security audit

Agent Attestation

Security checks for vulnerabilities and agentic risk

Overview

This skill is a coherent local agent reputation prototype, but it has real review-worthy risks around local file writes, private key storage, and trust claims that are not consistently enforced.

Review before installing or using this for any real trust decision. Treat it as a prototype unless the legacy v1/v2 modules are removed or clearly disabled, agent names are constrained to safe identifiers, key and KV storage are moved to protected directories with restrictive permissions, and private keys or sensitive identity data are protected at rest.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
attestation_system_v3.py:54
Finding
Agent Name Allows Key-File Path Traversal and Arbitrary File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `attestation_system_v3.py:54-62, 76-96` **Vulnerability Type**: Path traversal and unsafe file access **Risk Level**: High ### Vulnerable Code ```python # Save to files private_path = self.keys_dir / f"{agent_name}.key" public_path = self.keys_dir / f"{agent_name}.pub" with open(private_path, 'wb') as f: f.write(private_pem) with open(public_path, 'wb') as f: f.write(public_pem) ``` The same unsanitized value is used when loading keys: ```python def load_private_key(self, agent_name: str) -> Optional[ed25519.Ed25519PrivateKey]: """Load private key from file.""" private_path = self.keys_dir / f"{agent_name}.key" if not private_path.exists(): return None with open(private_path, 'rb') as f: private_pem = f.read() return serialization.load_pem_private_key( private_pem, password=None, backend=default_backend() ) def load_public_key(self, agent_name: str) -> Optional[ed25519.Ed25519PublicKey]: """Load public key from file.""" public_path = self.keys_dir / f"{agent_name}.pub" if not public_path.exists(): return None with open(public_path, 'rb') as f: public_pem = f.read() return serialization.load_pem_public_key( public_pem, backend=default_backend() ) ``` ### Technical Analysis `agent_name` is incorporated directly into private- and public-key paths without validation. A value containing `../` can escape `keys_dir`, while an absolute path causes `pathlib` to disregard the configured base directory. The code also follows filesystem symbolic links. When the computed key pair does not already exist, object initialization generates a new pair and writes both files using `wb`, which truncates existing writable files. If both attacker-selected files already exist and contain valid PEM keys, the loading path can instead cause the application to consume keys outside the intended ke ...[truncated 1607 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Validate `agent_name` against a strict identifier allowlist, such as `^[A-Za-z0-9_-]{1,64}$`. - Reject absolute paths, path separators, null bytes, `.` components, and `..` components. - Prefer deriving filenames from a full cryptographic hash of the logical agent identifier. - Resolve the resulting path and verify that it remains under the resolved key directory before every read or write. - Reject symbolic-link targets and use safe file-opening flags such as `O_NOFOLLOW` where supported. - Create new key files atomically with exclusive creation rather than truncating existing files. - Separate logical identity names from physical key-store filenames. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
attestation_system_v3.py:35
Finding
Ed25519 Private Keys Are Stored Unencrypted Without Enforced Access Permissions<![CDATA[ ## Vulnerability Details **File Location**: `attestation_system_v3.py:35-62` **Vulnerability Type**: Insecure storage of cryptographic private keys **Risk Level**: High ### Vulnerable Code ```python def __init__(self, keys_dir: str = "./keys"): self.keys_dir = Path(keys_dir) self.keys_dir.mkdir(parents=True, exist_ok=True) def generate_keypair(self, agent_name: str) -> Tuple[str, str]: """Generate a new Ed25519 keypair for an agent.""" private_key = ed25519.Ed25519PrivateKey.generate() public_key = private_key.public_key() # Serialize keys private_pem = private_key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption() ) public_pem = public_key.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo ) # Save to files private_path = self.keys_dir / f"{agent_name}.key" public_path = self.keys_dir / f"{agent_name}.pub" with open(private_path, 'wb') as f: f.write(private_pem) with open(public_path, 'wb') as f: f.write(public_pem) ``` ### Technical Analysis Private signing keys are serialized using `serialization.NoEncryption()` and written through ordinary `open()` calls. The code does not explicitly create the key directory with mode `0700` or the private-key file with mode `0600`. Actual accessibility therefore depends on the process umask, inherited directory permissions, and the caller-selected location. The comments recommend a secure location, but comments do not enforce confidentiality. If the key directory is shared, backed up insecurely, or created under a permissive umask, another local principal may obtain the private key. ### Attack Path 1. The application initializes `AttestationSystemV3` in a directory accessible to another local user, workspace participant, container process, or ...[truncated 728 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the key directory with owner-only permissions (`0700`) and verify its ownership and mode before use. - Create private-key files atomically with owner-only permissions (`0600`), independent of the process umask. - Refuse to use key files owned by another user or accessible to group/other principals. - Encrypt PKCS#8 private keys with a securely supplied passphrase where operationally feasible. - Prefer an operating-system keyring, hardware-backed keystore, or dedicated secret manager. - Add key rotation, revocation, and compromise-recovery procedures. - Never place private keys in shared workspaces, source repositories, or broadly accessible temporary directories. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
attestation_system_v2.py:73
Finding
Legacy Attestations Use Forgeable Hashes and Are Scored Without Authentication<![CDATA[ ## Vulnerability Details **File Location**: `attestation_system.py:42-44, 65-82`; `attestation_system_v2.py:73-75, 80-115` **Vulnerability Type**: Missing cryptographic authentication **Risk Level**: High ### Vulnerable Code In the v1 implementation, the alleged signature is only a truncated unkeyed hash: ```python # Create signature hash (placeholder for real crypto) canonical = json.dumps(attestation, sort_keys=True, ensure_ascii=False) attestation["signature"] = hashlib.sha256(canonical.encode()).hexdigest()[:16] ``` The verification method returns matching records without checking that value: ```python def verify_agent(self, agent_name: str, filepath: str = None) -> dict: """Verify an agent's reputation""" filepath = Path(filepath) if filepath else self.attestations_file if not filepath.exists(): return {"found": False, "message": "No attestations found"} with open(filepath, 'r', encoding='utf-8') as f: data = json.load(f) attestations = [a for a in data.get("attestations", []) if a.get("subject") == agent_name] return { "found": len(attestations) > 0, "count": len(attestations), "attestations": attestations } ``` The v2 implementation repeats the unkeyed hash construction: ```python # Create hash signature canonical = json.dumps(attestation, sort_keys=True, ensure_ascii=False) attestation["signature"] = hashlib.sha256(canonical.encode()).hexdigest()[:16] ``` Its scoring path also accepts records without signature verification: ```python for att in attestations: if att.get("subject") == agent_name: if self.is_expired(att.get("timestamp", "")): expired_attestations.append(att) else: valid_attestations.append(att) total_score = 0.0 vouch_count = 0 for att in valid_attestations: task_value = att.get("task_value", "medium") weight = TASK_VALUE_WEIGHTS.get(task_value, 1.0) # Base score score = ...[truncated 1747 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove or disable the v1 and v2 implementations in production. - Clearly mark legacy formats as unauthenticated and never include them in trusted reputation calculations. - Require Ed25519 verification for every attestation before display, storage, migration, or scoring. - Reject unknown and legacy version identifiers by default. - Bind the claimed attestor identity and public-key fingerprint to a trusted key registry rather than trusting self-declared fields. - If migration is required, perform it through an explicitly trusted process and re-sign migrated records. - Add negative tests proving that unsigned, modified, and self-declared legacy records are rejected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
handoff_kv.py:31
Finding
Persistent Identity and Reputation Records Lack Confidentiality and Integrity Protection<![CDATA[ ## Vulnerability Details **File Location**: `handoff_kv.py:31-50, 67-82, 153-190` **Vulnerability Type**: Insecure persistent storage **Risk Level**: Medium ### Vulnerable Code ```python def __init__(self, kv_dir: str = "./attestation_kv"): self.kv_dir = Path(kv_dir) self.kv_dir.mkdir(parents=True, exist_ok=True) # Manifest tracks all keys and their metadata self.manifest_file = self.kv_dir / "manifest.json" self.manifest = self._load_manifest() def _load_manifest(self) -> Dict: """Load the manifest of all keys.""" if self.manifest_file.exists(): with open(self.manifest_file, 'r', encoding='utf-8') as f: return json.load(f) return {"keys": {}, "last_updated": None} def _save_manifest(self) -> None: """Save the manifest.""" self.manifest["last_updated"] = datetime.now(timezone.utc).isoformat() with open(self.manifest_file, 'w', encoding='utf-8') as f: json.dump(self.manifest, f, indent=2, ensure_ascii=False) ``` Values are likewise written directly as plaintext JSON: ```python with open(value_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) ``` Identity and reputation are restored without an integrity check: ```python def load_identity(self) -> Optional[Dict]: """Load agent identity from KV.""" data = self.kv.get_with_metadata(self.identity_key) if data: return data.get("value") return None def load_reputation(self) -> Dict: """Load reputation data.""" return self.kv.get(self.reputation_key, {"score": 0}) def load_attestations(self) -> List[Dict]: """Load all attestations.""" return self.kv.get(self.attestations_key, []) ``` ### Technical Analysis The handoff store persists email addresses, identity metadata, reputation values, and complete attestations as unencrypted JSON. As with the key store, directory and file permissions are not explicitly restricted. Records do not carry a message aut ...[truncated 1513 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the KV directory with mode `0700` and files with mode `0600`. - Verify file ownership and permissions before loading persistent state. - Encrypt sensitive identity fields and attestations at rest using a protected key. - Authenticate every record with a MAC or digital signature and reject modified records. - Use atomic replacement: write to a restricted temporary file, flush and synchronize it, then rename it over the destination. - Add inter-process locking or use a transactional embedded database for concurrent access. - Add version counters or signed monotonic metadata to detect rollback. - Validate the complete restored schema before using identity or reputation values. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
attestation_system_v3.py:369
Finding
v3 Scoring Bypasses Input Validation and Accepts Future Timestamps and Unbounded Stakes<![CDATA[ ## Vulnerability Details **File Location**: `attestation_system_v3.py:268-300, 369-429` **Vulnerability Type**: Incomplete validation of signed reputation data **Risk Level**: Medium ### Vulnerable Code The standalone validation method detects future timestamps: ```python # 6. Check for future timestamps try: att_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) now = datetime.now(timezone.utc) if att_time > now: result["issues"].append("Timestamp is in the future") result["confidence"] *= 0.3 except: pass ``` However, verification only calls `is_expired()` and does not call `validate_input()`: ```python # Check expiration timestamp = attestation.get("timestamp") if timestamp and not self.is_expired(timestamp): result["not_expired"] = True else: result["errors"].append("Attestation expired") result["valid"] = result["signature_valid"] and result["not_expired"] ``` Scoring likewise omits schema validation and adds an unchecked stake: ```python for att in attestations: if att.get("subject") == agent_name: # Check expiration if self.is_expired(att.get("timestamp", "")): expired_attestations.append(att) continue # Check signature if requested if verify_signatures: signer = att.get("attestor", {}).get("name") if not self.verify(att, signer): invalid_signatures.append(att) continue valid_attestations.append(att) total_score = 0.0 vouch_count = 0 for att in valid_attestations: task_value = att.get("task_value", "medium") weight = TASK_VALUE_WEIGHTS.get(task_value, 1.0) score = 1.0 * weight if att.get("stake", {}).get("vouched", False): score += att.get("stake", {}).get("reputation_at_stake", 0.5) vouch_count += 1 total_score += score ``` ### Technical Analysis `validate_input()` is advisory and disconnected from both `verify_attesta ...[truncated 1662 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Make schema and semantic validation mandatory before verification results or scoring are returned. - Reject timestamps beyond a narrowly configured clock-skew allowance. - Validate `expires_at` and require it to be later than `timestamp` but no later than the maximum rolling-window duration. - Require stake values to be numeric, finite, non-negative, and below a defined maximum. - Verify that the signer owns sufficient reputation before accepting a stake. - Reject invalid `task_value`, domain, version, attestor, subject, and nested-object types rather than silently applying defaults. - Catch numeric conversion errors and return structured validation failures rather than allowing scoring exceptions. - Add tests for future timestamps, ignored expiration fields, negative values, huge values, strings, `NaN`, and infinity. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (16)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
Advertising Ed25519 signatures and input validation when they are absent creates a false assurance boundary around identity, integrity, and data safety. Combined with undeclared local persistence of identity, reputation, and attestation data, this can enable forgery, corruption, or unintended retention of sensitive trust-state information.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
Advertising Ed25519 signatures and input validation when they are absent creates a false assurance boundary around identity, integrity, and data safety. Combined with undeclared local persistence of identity, reputation, and attestation data, this can enable forgery, corruption, or unintended retention of sensitive trust-state information.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
Advertising Ed25519 signatures and input validation when they are absent creates a false assurance boundary around identity, integrity, and data safety. Combined with undeclared local persistence of identity, reputation, and attestation data, this can enable forgery, corruption, or unintended retention of sensitive trust-state information.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
Advertising Ed25519 signatures and input validation when they are absent creates a false assurance boundary around identity, integrity, and data safety. Combined with undeclared local persistence of identity, reputation, and attestation data, this can enable forgery, corruption, or unintended retention of sensitive trust-state information.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The code claims to provide Ed25519-backed attestations, but the 'signature' is only a truncated SHA-256 digest over the JSON payload. This is not a digital signature because it uses no private key and provides no authenticity, so any party can forge attestations that appear valid. In a reputation or trust system, this enables impersonation and fabricated trust relationships.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The skill metadata claims Ed25519 signatures, but the implementation only stores the first 16 hex characters of a SHA-256 digest over the JSON object and never performs asymmetric signing or verification. This provides no authentic proof of origin because anyone can recompute the digest for forged attestations, and the truncation further weakens integrity by reducing collision resistance.

Lp3

Medium
Category
MCP Least Privilege
Confidence
96% confidence
Finding
The skill metadata declares no explicit tool scope or permissions, yet the described implementation requires filesystem read/write access. This creates a trust and containment problem because operators cannot accurately assess what the skill may access or modify, and the skill could persist sensitive data or alter local state without clear declaration.

Description-Behavior Mismatch

Medium
Confidence
87% confidence
Finding
The manifest suggests a portable attestation system with 'handoff KV', implying some key-value handoff or exchange mechanism. The code persists attestations solely to a local file named attestations.json and exports that file's contents, with no KV abstraction or handoff-oriented storage behavior present.

Intent-Code Divergence

Medium
Confidence
97% confidence
Finding
The method is named and documented as verifying an agent's reputation, but it merely returns attestations whose subject matches the agent name. Because it performs no signature verification, provenance checks, or trust evaluation, callers may treat untrusted records as validated reputation data, enabling spoofed or malicious attestations to influence decisions.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The code advertises input validation, but it only normalizes the reason string and does not validate key fields such as subject, task_value, context structure, timestamp format at creation, or stake_amount bounds. In a reputation and attestation system, unvalidated inputs can enable malformed or manipulated attestations, score inflation, inconsistent processing, and downstream trust decisions based on attacker-controlled data.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The code writes unencrypted Ed25519 private keys to disk using predictable per-agent filenames and default filesystem behavior, with no permission hardening, passphrase protection, or user-facing warning. If the host is multi-user, the working directory is exposed, backups are accessible, or the path is attacker-controlled, compromise of the private key allows forged attestations and complete trust impersonation.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
This code persists broader identity data, including email and arbitrary metadata, to local disk in plaintext as part of an agent attestation/reputation system. In this skill context, durable identity storage is core functionality, but storing personal or sensitive fields beyond the minimum necessary increases privacy and data-exposure risk if the KV directory is readable by other users, included in backups, or checked into a workspace.

Description-Behavior Mismatch

Low
Confidence
95% confidence
Finding
The manifest description claims three major areas: Ed25519 signatures, input validation, and handoff KV. This file clearly implements signatures and input validation, but there is no code for any key-value handoff storage, retrieval, or exchange mechanism; instead it focuses on local attestation creation, verification, and scoring.

Intent-Code Divergence

Low
Confidence
81% confidence
Finding
The inline comment frames duplicate-attestation checking as something that 'would be checked against a registry in production,' implying this validation step is part of the intended behavior. In reality, the function performs no duplicate detection whatsoever, so the documentation suggests a protective behavior that the code does not provide.

Missing User Warnings

Low
Confidence
82% confidence
Finding
This code persists identity metadata and reputation information via save_identity and save_reputation, which are file-writing operations affecting user data. While the file contains general security notes about using secure locations, it does not clearly warn the user at the point of persistence that these example calls will store data on disk.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The docstring for get_history states that the store keeps the last 10 versions. However, the implementation explicitly notes that version storage is not implemented and returns only the current record if present, which directly contradicts the documented behavior.

Static analysis

No suspicious patterns detected.