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## 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
## 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## 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
## 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## 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
## 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## 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
## 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## 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
## 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.

Missing User Warnings

Medium
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.

Natural-Language Policy Violations

Medium
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.

Missing User Warnings

Medium
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.

Intent-Code Divergence

Low
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.

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.

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.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"start": "node dist/index.js"
  },
  "dependencies": {
    "loro": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
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.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"loro": "^1.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}
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.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0"
  }
}
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.

Static analysis

No suspicious patterns detected.