Back to skill

Security audit

Lobster Comm

Security checks for vulnerabilities and agentic risk

Overview

This is a real peer-to-peer agent messaging skill, but its authentication and local control boundaries are too weak for trusted agent-to-agent use without review.

Install only if you understand that messages cross your Tailnet and are stored on disk. Do not use it for secrets or autonomous task execution until peer key pinning, source validation, authenticated IPC, restrictive key storage, and clearer cleanup/disable steps are added. Avoid enabling auto-start or the Windows service form on shared machines.

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/lcp_node.py:261
Finding
Peer Identity Impersonation Through Self-Asserted Public Keys<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lcp_crypto.py:48-64`, `scripts/lcp_node.py:261-294`, `scripts/windows/lcp_crypto_win.py:49-64`, and `scripts/windows/lcp_node_win.py:241-263` **Vulnerability Type**: Missing peer-key authentication **Risk Level**: High ### Vulnerable Code ```python def verify_message(msg_dict): """Verify the signature in the security block""" if "security" not in msg_dict: raise ValueError("Missing security block") sec = msg_dict["security"] if sec.get("algo") != "ed25519": raise ValueError(f"Unsupported algorithm: {sec.get('algo')}") pubkey_bytes = base64.b64decode(sec["pubkey"]) sig_bytes = base64.b64decode(sec["signature"]) payload = _canonicalize(msg_dict) verify_key = VerifyKey(pubkey_bytes) try: verify_key.verify(payload, sig_bytes) return True except BadSignatureError: return False ``` The verified message is subsequently stored without checking whether the embedded key belongs to the configured peer: ```python elif msg_type == MSG_DATA: # 1. ACK immediately udp_send_raw(pack(MSG_ACK, seq), addr[0]) # 2. Parse JSON try: msg = json.loads(payload.decode('utf-8')) except json.JSONDecodeError: log.warning(f"Invalid JSON from {addr}") continue msg_id = msg.get("id", "") # 3. Duplicate check if is_seen(msg_id): log.info(f"Duplicate msg {msg_id[:8]}, skipped") continue # 4. Verify signature try: if not verify_message(msg): log.warning(f"Invalid signature from {addr}") continue except Exception as e: log.warning(f"Signature check error: {e}") continue # 5. Save to inbox mark_seen(msg_id) fname = f"msg_{msg_id[:8]}.json" fpath = os.path.join(INBOX_DIR, fname) with open(fpath, 'w', encoding='utf-8') as f: json.dump(msg, f, ensure_ascii=False, inden ...[truncated 2467 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Provision or configure the expected Ed25519 public key for each peer. 2. Compare `security.pubkey` against the pinned key before signature verification and reject unknown or changed keys. 3. If pre-provisioning is unavailable, implement an explicit trust-on-first-use registry with visible key fingerprints and protected persistent storage. 4. Require the UDP source address to match the configured peer address. Treat this only as defense in depth, not as a substitute for cryptographic identity. 5. Validate that the authenticated key is authorized for the signed `from` identity. 6. Require the `to` field to identify the local node. 7. Consider moving the public key out of individual messages and using a configured key identifier. 8. Add negative tests proving that a correctly signed message from an unknown key is rejected. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/lcp_node.py:251
Finding
Spoofable and Premature Delivery Acknowledgments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lcp_node.py:251-263` and `scripts/windows/lcp_node_win.py:231-242` **Vulnerability Type**: Unauthenticated protocol acknowledgments **Risk Level**: Medium ### Vulnerable Code ```python if msg_type == MSG_ACK: with ack_lock: ev = pending_acks.get(seq) if ev: ev.set() elif msg_type == MSG_HEARTBEAT: # Reply with ACK to heartbeat udp_send_raw(pack(MSG_ACK, seq)) log.debug(f"Heartbeat from {addr}, replied ACK") elif msg_type == MSG_DATA: # 1. ACK immediately udp_send_raw(pack(MSG_ACK, seq), addr[0]) # 2. Parse JSON try: msg = json.loads(payload.decode('utf-8')) except json.JSONDecodeError: log.warning(f"Invalid JSON from {addr}") continue ``` Sequence numbers are predictable: ```python def next_seq(): global seq_counter with seq_lock: seq_counter = (seq_counter + 1) % 0xFFFFFFFF return seq_counter ``` ### Technical Analysis ACK packets contain only the protocol magic value, a monotonically increasing sequence number, and the ACK type. They are neither signed nor otherwise authenticated. The receiver accepts an ACK without checking its source address against the configured peer. Sequence numbers begin at zero each time the daemon starts and increment predictably. An attacker who observes traffic can identify the active sequence number directly, while an attacker without observation may attempt to predict or probe likely values. The DATA receiver also transmits its ACK before parsing JSON, checking for duplicates, validating the signature, or durably storing the message. The sender therefore treats packet receipt as successful delivery even when the receiver subsequently rejects the payload. ### Attack Path 1. The sender transmits a DATA packet and registers a pending event for its sequence number. 2. An attacker observes or predicts that sequence number. 3. The attacker sends an LCP ...[truncated 1102 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject ACKs unless their source address and source port match the configured peer. 2. Cryptographically authenticate ACK packets or carry acknowledgments inside signed protocol messages. 3. Bind each ACK to an unpredictable transaction identifier and a hash of the acknowledged message. 4. Use cryptographically random transaction identifiers instead of restart-predictable sequence numbers. 5. Send a positive delivery ACK only after JSON parsing, peer authentication, duplicate handling, and durable inbox storage succeed. 6. Define a separate negative acknowledgment for malformed, unauthorized, or unstoreable messages. 7. Do not mark messages as delivered solely because an unauthenticated packet with a matching sequence number was received. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/lcp_crypto.py:16
Finding
Ed25519 Private Seed Created Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/lcp_crypto.py:16-28` **Vulnerability Type**: Insecure private-key storage **Risk Level**: Medium ### Vulnerable Code ```python def get_or_create_keys(): """Load or generate Ed25519 keys""" if not os.path.exists(KEY_DIR): os.makedirs(KEY_DIR, exist_ok=True) if os.path.exists(IDENTITY_FILE): with open(IDENTITY_FILE, "rb") as f: seed = f.read() signing_key = SigningKey(seed) else: signing_key = SigningKey.generate() with open(IDENTITY_FILE, "wb") as f: f.write(signing_key.encode()) ``` ### Technical Analysis The code stores the raw 32-byte Ed25519 private seed in `keys/identity.pem`. Despite the `.pem` extension, the value is unencrypted raw key material. The key directory and file are created using process-default permissions. No explicit restrictive mode is supplied, and no post-creation permission validation or `chmod` operation is performed. With a conventional permissive umask, the directory may be created as `0755` and the file as `0644`, allowing other local users to read the identity seed. There is also a check-then-create sequence around the key file. Secure atomic creation is not used, which creates avoidable race and replacement risks in a locally shared environment. ### Attack Path 1. The daemon or signing helper runs under a permissive umask and creates `keys/identity.pem`. 2. Another local account or process with filesystem access reads the raw private seed. 3. The attacker reconstructs the signing key using `SigningKey(seed)`. 4. The attacker can generate signatures indistinguishable from those produced by the legitimate node. 5. If peer-key pinning is deployed, the stolen key allows the attacker to continue impersonating the pinned identity until the key is revoked and replaced. ### Impact Assessment A local low-privilege user may obtain the node's long-term signing identity where filesyste ...[truncated 370 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the key directory with mode `0700`. 2. Create the key file atomically using `os.open` with `O_WRONLY | O_CREAT | O_EXCL` and mode `0600`. 3. Set a restrictive umask while creating key material. 4. Validate the ownership and permissions of existing key files before loading them, and refuse to use a file readable or writable by unauthorized users. 5. Avoid following symbolic links and verify that the key path is a regular file owned by the daemon account. 6. Use an operating-system key store or hardware-backed keystore where available. 7. Add a documented key-rotation and revocation process for compromised identities. 8. Apply equivalent user-specific ACL protections to the Windows key implementation. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/windows/lcp_node_win.py:276
Finding
Unauthenticated Windows IPC Allows Local Message Disclosure and Impersonation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/windows/lcp_node_win.py:276-325` and `scripts/windows/lcp_node_win.py:331-340` **Vulnerability Type**: Missing local IPC authorization **Risk Level**: High ### Vulnerable Code ```python def handle_ipc_client(conn): try: raw = conn.recv(65536) if not raw: return req = json.loads(raw.decode('utf-8')) cmd = req.get("cmd", "").upper() if cmd == "SEND": payload = req.get("payload", {}) ok, msg = send_message(payload) resp = {"ok": ok, "id": msg.get("id")} elif cmd == "CHECK": messages = [] for fp in sorted(glob.glob(os.path.join(INBOX_DIR, "msg_*.json"))): try: with open(fp, 'r', encoding='utf-8-sig') as f: m = json.load(f) m["_filepath"] = fp messages.append(m) except: pass resp = {"count": len(messages), "messages": messages} elif cmd == "ACK": archived = 0 for fp in glob.glob(os.path.join(INBOX_DIR, "msg_*.json")): try: os.rename(fp, os.path.join(INBOX_ARCHIVE, os.path.basename(fp))) archived += 1 except: pass resp = {"archived": archived} elif cmd == "STATUS": inbox_count = len(glob.glob(os.path.join(INBOX_DIR, "msg_*.json"))) resp = { "peer": PEER_NAME, "peer_ip": PEER_IP, "peer_online": is_peer_online(), "peer_last_seen": peer_last_seen, "inbox_count": inbox_count, "uptime": time.time() - start_time, } else: resp = {"error": f"Unknown: {cmd}"} conn.sendall(json.dumps(resp, ensure_ascii=False).encode('utf-8')) ``` The IPC endpoint is restrict ...[truncated 2394 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace loopback TCP IPC with a Windows named pipe protected by a discretionary access control list restricted to the intended service account and authorized client identity. 2. If TCP IPC must be retained, require a cryptographically random authentication token stored with user-specific ACLs. 3. Use a challenge-response mechanism rather than transmitting a reusable token without additional protection. 4. Separate authorization for inbox reading, message sending, status inspection, and archival operations. 5. Require explicit message identifiers for archival instead of allowing an unauthenticated command to archive the entire inbox. 6. Rate-limit IPC requests and log authenticated client identities and sensitive operations. 7. Avoid returning internal filesystem paths unless they are required by the caller. 8. Add tests proving that an unauthorized local process cannot invoke `CHECK`, `SEND`, or `ACK`. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (12)

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill advertises capabilities that imply file access and shell-driven execution patterns, but it does not declare any explicit tool scope or permission boundaries. In an agent setting, this can cause the host to grant broader-than-necessary access implicitly, increasing the chance of unintended file modification, command execution, or data exposure when the skill is invoked.

Vague Triggers

Medium
Confidence
91% confidence
Finding
The trigger phrases are very broad for a skill that enables cross-machine communication and local daemon control, making accidental or overly frequent activation more likely. That matters because invocation could lead to network transmission, local state changes, and persistence-related setup in contexts where the user did not clearly intend to use a P2P messaging subsystem.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill description promotes signed bot-to-bot messaging, local IPC, and storage in inbox/outbox directories, but it does not warn users that content will traverse the network and be persisted locally. Users may provide sensitive data under the assumption of transient local processing, creating confidentiality and retention risks if messages are stored, forwarded, or later accessed by other local processes.

Session Persistence

Medium
Category
Rogue Agent
Content
## Auto-start (macOS)

Create a LaunchAgent plist pointing to `lcp_node.py` with `RunAtLoad=true` and `KeepAlive` for crash recovery.

## Auto-start (Windows)
Confidence
84% confidence
Finding
The auto-start guidance instructs users to create a persistent LaunchAgent with `RunAtLoad` and `KeepAlive`, which establishes session persistence for a network-capable daemon. While persistence is presented as operational convenience, it increases exposure by keeping the service available continuously and can make unauthorized or unintended communication survive reboots and user logins.

Intent-Code Divergence

Medium
Confidence
96% confidence
Finding
The top-level docstring describes the script as archiving processed inbox messages via LCP daemon IPC, which indicates daemon-mediated behavior. In practice, lines L51-L53 invoke a direct filesystem-based fallback when the IPC socket is unavailable, contradicting the stated execution model rather than merely omitting a minor detail.

Description-Behavior Mismatch

Medium
Confidence
94% confidence
Finding
The module docstring says it archives processed inbox messages via the LCP daemon IPC and replaces the prior ACK path for UDP mode, implying daemon-mediated control as the mechanism. However, the fallback path directly renames message files in the inbox to the archive directory, so the script performs local filesystem mutation independently of the daemon rather than only using IPC-based acknowledgment.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This code generates and persists an Ed25519 private key to disk in `keys/identity.pem`, which is a security-sensitive file write affecting local credentials. Although the function has an internal docstring, there is no user-facing prompt, warning, or disclosure at the point of creation to inform users that persistent key material will be stored locally.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The ACK IPC command moves all inbox message files into the archive directory, which changes local user data state, but there is no confirmation prompt or explicit user-facing warning at the point of execution. The surrounding docstrings and logs describe the action only as an internal implementation detail, not as a disclosure to the user invoking the command.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The code persists the Ed25519 private key unencrypted to a predictable path on disk beside the script, with no permission hardening, secure storage API, or operator warning about key sensitivity. In this skill's context, that key is the trust anchor for bot-to-bot message signing, so local theft of the file would let an attacker impersonate the agent and forge authenticated messages across the distributed system.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The local IPC command `ACK` moves all inbox message files into the archive directory, which changes message state in bulk and may affect user data handling. Although the code executes the operation, there is no confirmation prompt or explicit user-facing warning tied to this destructive state transition.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The code forces timestamps to use UTC+8 via a hard-coded timezone offset when constructing outgoing messages. This imposes a locale-specific behavior without giving the user a choice or documenting why that locale is required.

Missing User Warnings

Low
Confidence
80% confidence
Finding
The UDP listener persists inbound message contents to `INBOX_DIR` as JSON files, which stores potentially sensitive transmitted data on disk. While logging exists for receipt, there is no visible warning or disclosure to the user in prompts, comments, or documentation within this file that messages will be stored locally.

Static analysis

No suspicious patterns detected.