Back to skill

Security audit

Operator Skill

Security checks for vulnerabilities and agentic risk

Overview

This skill has a coherent collaboration-storage purpose, but its implementation gives session IDs and caller-chosen keys too much authority over persisted session data.

Review this skill before installing. It should be used only in an isolated workspace until it validates session IDs, enforces session ownership or participant ACLs, binds signatures to authorized session keys and nonces, adds payload and storage limits, and fixes the loro versus loro-crdt dependency mismatch. Avoid storing secrets or sensitive collaboration data in it as written.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (5)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
index.ts:70
Finding

Path Traversal Through Unvalidated Session Identifiers

Content
View full analysis

Vulnerability Details

File Location: index.ts:70-91
Vulnerability Type: Path traversal and unauthorized filesystem access
Risk Level: High

Vulnerable Code

ts
const updatesPath = path.join(SESSIONS_PATH, `${sessionId}.updates`);
if (!fs.existsSync(updatesPath)) return { success: false, error: "Session updates log not found" };

const binaryUpdate = Buffer.from(updatePayload, 'base64');

fs.appendFileSync(updatesPath, binaryUpdate);
ts
async function loadTeam(params: any) {
  const { sessionId } = params;
  const snapshotPath = path.join(SESSIONS_PATH, `${sessionId}.snapshot`);
  const updatesPath = path.join(SESSIONS_PATH, `${sessionId}.updates`);

Technical Analysis

The caller-controlled sessionId is inserted into filesystem paths without validation or canonical containment checks. path.join() normalizes traversal segments but does not guarantee that the resulting path remains under SESSIONS_PATH.

A value containing components such as ../ can therefore escape data/sessions. The team.sync action can append data to an existing attacker-selected path whose final name ends in .updates. The team.load action can attempt to read attacker-selected .snapshot and .updates files outside the session directory.

The filename suffixes constrain the set of reachable files, but they do not prevent directory traversal or access outside the intended storage boundary.

Attack Path

  1. The attacker identifies an existing file outside data/sessions ending in .updates, or places a compatible .snapshot file in another accessible directory.
  2. The attacker supplies a traversal identifier such as ../../target as sessionId.
  3. path.join() resolves the resulting path outside SESSIONS_PATH.
  4. For team.sync, the attacker submits a signed payload and the application appends it to target.updates.
  5. For team.load, the application reads target.snapshot ...[truncated 762 chars]
Remediation
View remediation

Remediation Suggestions

  • Validate sessionId against the exact server-generated format, for example ^session-[0-9]+-[a-z0-9]{5}$.

  • Reject path separators, . components, encoded traversal sequences, null bytes, and unexpected characters.

  • Resolve the final path and verify containment before every filesystem operation:

    ts
    function sessionFile(sessionId: string, extension: string): string {
      if (!/^session-[0-9]+-[a-z0-9]{5}$/.test(sessionId)) {
        throw new Error("Invalid session ID");
      }
    
      const root = path.resolve(SESSIONS_PATH);
      const candidate = path.resolve(root, `${sessionId}.${extension}`);
    
      if (!candidate.startsWith(root + path.sep)) {
        throw new Error("Session path escapes storage directory");
      }
    
      return candidate;
    }
    
  • Open files using restrictive flags and permissions where appropriate.

  • Run the service under a dedicated, least-privileged operating-system account.

  • Add tests covering ../, absolute paths, repeated separators, encoded traversal attempts, and platform-specific separators.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
index.ts:86
Finding

Session State Can Be Loaded Without Authentication or Authorization

Content
View full analysis

Vulnerability Details

File Location: index.ts:86-113
Vulnerability Type: Missing object-level authorization
Risk Level: High

Vulnerable Code

ts
async function loadTeam(params: any) {
  const { sessionId } = params;
  const snapshotPath = path.join(SESSIONS_PATH, `${sessionId}.snapshot`);
  const updatesPath = path.join(SESSIONS_PATH, `${sessionId}.updates`);

  if (!fs.existsSync(snapshotPath)) return { success: false, error: "Session not found" };

  const doc = new LoroDoc();
  doc.import(fs.readFileSync(snapshotPath));

  if (fs.existsSync(updatesPath)) {
    const allUpdates = fs.readFileSync(updatesPath);
    if (allUpdates.length > 0) {
      doc.import(allUpdates);
    }
  }

  return {
    success: true,
    data: {
      payload: Buffer.from(doc.export({ mode: 'snapshot' })).toString('base64'),
      format: 'full-merged-snapshot'
    }
  };
}

The identifiers are generated as follows:

ts
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`;

Technical Analysis

team.load treats possession of a sessionId as sufficient authority to retrieve the full session state. It does not authenticate the caller, verify session membership, consult an access-control list, or require a read capability.

The session identifier includes a timestamp and only five pseudo-random base-36 characters generated with Math.random(). It should not be treated as an authorization credential. IDs may also be exposed through logs, collaboration messages, responses, or other application components.

The exploitability depends on how the exported run() function is exposed by the host platform. If untrusted callers can invoke it, any caller who obtains a valid session ID can load that session.

Attack Path

  1. An attacker obtains a session ID from logs, shared messages, another participant, or another information-disclosure channe ...[truncated 809 chars]
Remediation
View remediation

Remediation Suggestions

  • Authenticate every caller through the host platform.
  • Store session ownership and participant membership when a session is created or joined.
  • Enforce explicit read and write ACLs for every team.load and team.sync request.
  • Do not use an object identifier as an authorization credential.
  • Generate opaque identifiers with crypto.randomUUID() or crypto.randomBytes().
  • Consider issuing separate, revocable read and write capability tokens if identity-based ACLs are unavailable.
  • Return a uniform error for nonexistent and unauthorized sessions to reduce identifier enumeration.
  • Record security audit events for denied and successful session access.

T09 · Insecure Skill Coding Practices

Error
Location
index.ts:51
Finding

Attacker-Selected Public Keys Bypass Session Write Authorization

Content
View full analysis

Vulnerability Details

File Location: index.ts:51-78
Vulnerability Type: Improper cryptographic authorization and replay protection
Risk Level: High

Vulnerable Code

ts
async function syncTeam(params: any) {
  const { sessionId, updatePayload, publicKeyHex, signatureHex } = params;

  if (!sessionId || !updatePayload || !publicKeyHex || !signatureHex) {
    return { success: false, error: "Zero-Trust Violation" };
  }

  const verifyRes = await agentComm('message.verify', {
    publicKeyHex,
    payload: updatePayload,
    signatureHex
  }) as any;

  if (!verifyRes.success || !verifyRes.data!.verified) {
    return { success: false, error: "Signature Verification Failed" };
  }

  const updatesPath = path.join(SESSIONS_PATH, `${sessionId}.updates`);
  if (!fs.existsSync(updatesPath)) return { success: false, error: "Session updates log not found" };

  const binaryUpdate = Buffer.from(updatePayload, 'base64');

  fs.appendFileSync(updatesPath, binaryUpdate);
}

Technical Analysis

A valid digital signature proves only that the signer controls the private key corresponding to the supplied public key. Here, the caller supplies both publicKeyHex and the signed payload. The application does not verify that the public key is registered to the target session, belongs to an authorized participant, or is trusted by the service.

Consequently, any attacker can create their own Ed25519 key pair, sign a syntactically valid CRDT update, and satisfy the verification check. The signature covers only updatePayload; it does not bind the update to sessionId, a sequence number, a timestamp, or a nonce. A valid update can therefore also be replayed or potentially submitted to another session.

The actual implementation of message.verify resides in ../agent-comm-skill/index.ts, outside the audited project. Even if that implementation correctly verifies Ed25519 signatures, the missing ...[truncated 1210 chars]

Remediation
View remediation

Remediation Suggestions

  • Register trusted public keys when a participant is authorized to join a session.

  • Before signature verification, retrieve the authorized key from server-controlled session metadata rather than accepting arbitrary caller trust roots.

  • Sign a canonical envelope that binds all security-relevant fields:

    ts
    {
      sessionId,
      updateHash,
      signerId,
      sequenceNumber,
      nonce,
      issuedAt
    }
    
  • Verify that sessionId in the signed envelope matches the requested session.

  • Maintain a monotonically increasing sequence number or consumed-nonce set per signer and session.

  • Reject duplicate, stale, or cross-session messages.

  • Validate decoded data as a valid, bounded CRDT update before persistence.

  • Declare and audit the external agent-comm-skill dependency, including its key parsing and signature-verification behavior.

T08 · Insecure Dependencies

Warning
Location
package.json:10
Finding

Runtime Dependency Does Not Match the Imported CRDT Package

Content
View full analysis

Vulnerability Details

File Location: package.json:10-12; related import at index.ts:1 and lockfile entry at package-lock.json:458-469
Vulnerability Type: Dependency-name mismatch and unnecessary supply-chain exposure
Risk Level: Medium

Vulnerable Code

index.ts imports:

ts
import { LoroDoc } from 'loro-crdt';

package.json instead declares:

json
"dependencies": {
  "loro": "^1.0.0"
}

The lockfile resolves that declaration to an unrelated Express-based package:

json
"node_modules/loro": {
  "version": "1.0.6",
  "resolved": "https://registry.npmjs.org/loro/-/loro-1.0.6.tgz",
  "integrity": "sha512-l5cZhc4iCy7ZbzQKeEoYCSZtimM07oRHy/pGPizB6iEeDmUqFy5Y/bgFirgXyVwtUqfeme0ksh9sPcT8b/J+SA==",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.13.1",
    "express": "^4.13.0"
  }
}

Technical Analysis

The package imported by the application, loro-crdt, is absent from the runtime dependency manifest. Instead, the project installs loro version 1.0.6, which has an unrelated Express and body-parser dependency tree.

This mismatch prevents reproducible installation of the module that the source actually executes and adds unnecessary third-party code to the application’s supply chain. It is consistent with package-selection confusion or an incorrect package name, although the reviewed evidence does not establish that the installed loro package itself is malicious.

Attack Path

  1. An operator runs npm install or npm ci.
  2. npm installs the locked loro@1.0.6 package and its unrelated transitive dependencies.
  3. The expected loro-crdt package is not installed through the manifest.
  4. Compilation or runtime module resolution fails unless an undeclared copy happens to exist in the environment.
  5. If deployment procedures compensate by manually installing packages, using permissive resolution, or relying on hoisted depende ...[truncated 481 chars]
Remediation
View remediation

Remediation Suggestions

  • Remove the unrelated loro dependency.
  • Add the exact intended loro-crdt package after confirming its official package name, publisher, repository, and release provenance.
  • Pin a reviewed version rather than using a broad caret range for security-sensitive persistence code.
  • Regenerate package-lock.json using a clean environment.
  • Run npm ci and the TypeScript build in continuous integration to ensure all imports are declared and reproducible.
  • Use dependency allowlisting, lockfile integrity enforcement, provenance checks, and automated vulnerability scanning.
  • Review the CRDT parser for safe handling of untrusted serialized input before deployment.

T09 · Insecure Skill Coding Practices

Warning
Location
index.ts:75
Finding

Unbounded Synchronous Update Persistence Enables Resource Exhaustion

Content
View full analysis

Vulnerability Details

File Location: index.ts:75-78
Vulnerability Type: Unbounded input, synchronous I/O, and storage denial of service
Risk Level: Medium

Vulnerable Code

ts
const binaryUpdate = Buffer.from(updatePayload, 'base64');

// 使用 appendFileSync 确保原子化追加,性能极高
fs.appendFileSync(updatesPath, binaryUpdate);

Technical Analysis

The application decodes and permanently appends updatePayload without enforcing a maximum encoded or decoded size. It also lacks per-session quotas, global storage quotas, rate limits, update-count limits, and log compaction.

Buffer.from() allocates memory proportional to the decoded payload. appendFileSync() blocks the Node.js event loop until the write completes. Repeated large requests can therefore consume memory, increase request latency, block unrelated operations, and fill the filesystem.

Requiring a valid signature does not mitigate the issue because the public key is caller-selected. An attacker can sign arbitrarily large payloads with their own key.

Attack Path

  1. The attacker obtains or identifies an existing session ID.
  2. The attacker generates a key pair and creates a very large Base64 payload.
  3. The attacker signs the payload with the corresponding private key.
  4. The attacker repeatedly invokes team.sync.
  5. Each request allocates a decoded buffer and synchronously appends it to the update log.
  6. Repetition causes event-loop stalls, memory pressure, excessive load times, or filesystem exhaustion.
  7. team.load subsequently reads the entire unbounded log into memory, amplifying the denial-of-service condition.

Impact Assessment

A successful attack can degrade or terminate the Node.js process, exhaust the storage volume, prevent legitimate updates, and make affected sessions expensive or impossible to load. Because all sessions share the same process and storage root, resource exhaustion can affect the entire Skill r ...[truncated 174 chars]

Remediation
View remediation

Remediation Suggestions

  • Reject encoded payloads above a strict maximum before Base64 decoding.
  • Verify the decoded length and reject oversized CRDT updates.
  • Apply per-principal, per-session, and global rate limits.
  • Enforce per-session and global disk quotas.
  • Replace synchronous filesystem operations with controlled asynchronous writes.
  • Serialize writes per session to avoid concurrency and consistency problems.
  • Validate the CRDT update before appending it.
  • Implement periodic log compaction into an atomic snapshot and retain bounded recovery data.
  • Avoid reading the complete update log into one buffer during load; use a framed format and bounded incremental processing.
  • Monitor log growth, disk utilization, rejected payloads, and abnormal synchronization rates.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • 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 (10)

Known Vulnerable Dependency: path-to-regexp==0.1.12 — 1 advisory(ies): CVE-2024-45296 (path-to-regexp vulnerable to Regular Expression Denial of Service via multiple r)

High
Category
Supply Chain
Confidence
95% confidence
Finding

path-to-regexp 0.1.12 is a known ReDoS risk, and here it is transitively included through Express routing. Because route matching often processes attacker-controlled URL paths, a crafted request can trigger excessive backtracking and tie up the Node.js event loop, causing service degradation or denial of service.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill explicitly advertises persistent on-disk collaborative sessions, append-only logs, and crash-proof recovery, but it does not warn users that session contents may be retained indefinitely on disk. In an agent environment, this can lead to unintentional storage of sensitive prompts, secrets, or user data beyond the user's expectations, increasing privacy, compliance, and forensic exposure risk.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

This code includes natural-language instructions and operator-facing text in Chinese, such as the function doc comments and later console output, without any indication that Chinese is optional or required for a region-specific purpose. That creates a language-policy issue because the skill implicitly enforces one locale rather than offering user opt-in or a neutral default.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
86% confidence
Finding

The skill creates and writes persistent session data under data/sessions using writeFileSync, but the code provides no confirmation prompt and no user-facing disclosure that local files will be created. For code files, file writes that affect user or system data should have at least some visible warning, log, comment for users, or documented notice unless clearly covered by the skill description.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
77% confidence
Finding

The comment at L50-L52 describes the operation as limited to signature verification and append-only writing without reading or rewriting snapshots. However, the implementation also checks whether the updates log exists with fs.existsSync at L73, which is a filesystem read that affects externally observable behavior by returning a different error when the session is missing. This is a mild but real contradiction between the documented intent and actual behavior.

Content

No source excerpt is available for this finding.

Known Vulnerable Dependency: body-parser==1.20.4 — 1 advisory(ies): CVE-2026-12590 (body-parser vulnerable to denial of service when invalid limit value silently di)

Low
Category
Supply Chain
Confidence
69% confidence
Finding

The lockfile pins body-parser 1.20.4, which is flagged for a denial-of-service condition when an invalid limit value is handled unsafely. In this file, body-parser is not just incidental: it is pulled in directly by the runtime dependency 'loro' and is part of HTTP request parsing, so if the skill exposes an HTTP interface this can become remotely reachable.

Content

No source excerpt is available for this finding.

Known Vulnerable Dependency: qs==6.14.2 — 3 advisory(ies): CVE-2026-82417 (qs: Denial of Service via Attacker Controlled isBuffer); CVE-2026-8723 (qs has a remotely triggerable DoS: qs.stringify crashes with TypeError on null/u); CVE-2026-82562 (qs array-limit bypass via bracket-key comma parsing)

Low
Category
Supply Chain
Confidence
78% confidence
Finding

qs 6.14.2 is identified with multiple denial-of-service issues involving attacker-controlled parsing/stringifying edge cases. Since qs is commonly used to parse query strings and request bodies in Express/body-parser stacks, these flaws can be remotely triggered if untrusted input reaches the parser.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
93% confidence
Finding

The production dependency uses a caret range, which allows npm to resolve newer minor and patch releases than the one originally reviewed. This creates supply-chain risk because a compromised or breaking upstream release could be pulled into future installs without an explicit code change in this repository.

Content

Scanner excerpt · package.json (reported line 12)May include surrounding context.

json
"start": "node dist/index.js"
  },
  "dependencies": {
    "loro": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
84% confidence
Finding

The TypeScript devDependency is also unpinned, so build environments may resolve different compiler versions over time. While this is less dangerous than an unpinned runtime dependency, it still introduces supply-chain and build-integrity risk because a malicious or incompatible upstream release could affect compilation or developer workflows.

Content

Scanner excerpt · package.json (reported line 15)May include surrounding context.

json
"loro": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
79% confidence
Finding

The @types/node devDependency is unpinned, allowing type package updates to be consumed automatically. The security impact is limited because it is a development-only package, but it can still affect build reproducibility and exposes a smaller supply-chain attack surface in developer or CI environments.

Content

Scanner excerpt · package.json (reported line 16)May include surrounding context.

json
},
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}