Back to skill

Security audit

chitin-chronicle

Security checks for vulnerabilities and agentic risk

Overview

This appears to be a real editorial coordination skill, but its local helper script can run unintended shell commands or write outside its state folder when given crafted inputs.

Review this skill before installing. Do not use the claim, release, or publish commands with untrusted or free-form IDs, channels, titles, URLs, or OPENCLAW_AGENT values until the helper is fixed to validate identifiers, avoid shell command strings, constrain file paths, and stage only intended files. Avoid adding the boot hook or shell alias in a sensitive workspace until those issues are addressed.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (4)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/editorial.js:25
Finding
Shell Command Injection Through Git Commit Messages## Vulnerability Details **File Location**: `scripts/editorial.js`, lines 25-31; untrusted data reaches the vulnerable function from lines 94-116, 118-131, and 133-180 **Vulnerability Type**: OS command injection **Risk Level**: High ### Vulnerable Code ```js function gitCommit(message) { try { execSync(`git -C ${EDITORIAL_DIR} add .`, { stdio: 'ignore' }); execSync(`git -C ${EDITORIAL_DIR} commit -m "${message}"`, { stdio: 'ignore' }); } catch (e) { // Ignore commit failures (no changes, not in git repo, etc.) } } ``` Attacker-controlled command arguments and environment values are incorporated into `message`, for example: ```js const agent = process.env.OPENCLAW_AGENT || process.env.USER || 'unknown'; // ... gitCommit(`editorial: ${agent} claimed ${contentId} for ${action} on ${channel}`); ``` Equivalent untrusted interpolation also occurs when releasing and publishing content. ### Technical Analysis `execSync()` receives a single command string, so Node.js executes it through a shell. The commit message contains values derived from CLI arguments—`contentId`, `action`, and `channel`—and from `OPENCLAW_AGENT` or `USER`. None of these values are validated or safely escaped. Wrapping the message in double quotes does not neutralize shell syntax. POSIX shells still evaluate command substitutions such as `$(command)` and backticks inside double-quoted text. An attacker can therefore cause commands to run before `git commit` is invoked. The surrounding `try/catch` does not mitigate the vulnerability. Shell expansion occurs before Git processes its arguments, and any injected command may already have completed even if the subsequent Git operation fails. ### Attack Path 1. An attacker gains the ability to invoke a mutating CLI command or influence its arguments or environment. 2. The attacker supplies shell substitution syntax in `content-id`, `action`, `channel`, or `OPENCLAW_AGENT`, such as a value containing `$(attacker_command)`. 3. ...[truncated 944 chars]
Remediation
## Remediation Suggestions Avoid invoking Git through a shell. Use `execFileSync()` or `spawnSync()` with an explicit argument array: ```js const { execFileSync } = require('child_process'); function gitCommit(message) { try { execFileSync('git', ['-C', EDITORIAL_DIR, 'add', '.'], { stdio: 'ignore' }); execFileSync('git', ['-C', EDITORIAL_DIR, 'commit', '-m', message], { stdio: 'ignore' }); } catch (error) { // Log or handle the failure appropriately. } } ``` Additionally: 1. Validate `contentId`, `action`, `channel`, and agent identity using strict allowlists and length limits. 2. Do not treat shell escaping as the primary fix; eliminating shell parsing is safer. 3. Distinguish expected Git outcomes, such as “nothing to commit,” from unexpected failures rather than silently suppressing every error. 4. Add regression tests containing `$()`, backticks, quotes, semicolons, newlines, and option-like values. 5. Use a controlled identity value rather than falling back to arbitrary environment data where feasible.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/editorial.js:69
Finding
Path Traversal in Claim File Creation and Archiving## Vulnerability Details **File Location**: `scripts/editorial.js`, lines 69-74, 110-113, 124-127, and 176-178 **Vulnerability Type**: Path traversal leading to unauthorized file write or move **Risk Level**: High ### Vulnerable Code Claim creation constructs a pathname directly from untrusted values: ```js const filename = `${contentId}-${agent}.claim`; const filepath = path.join(CLAIMS_DIR, filename); writeJSON(filepath, claim); ``` Release and publication operations use the same unsafe filename pattern: ```js const agent = process.env.OPENCLAW_AGENT || process.env.USER || 'unknown'; const filename = `${contentId}-${agent}.claim`; archiveClaim(filename); ``` The archive operation joins that filename to trusted base directories without containment validation: ```js function archiveClaim(filename) { const srcPath = path.join(CLAIMS_DIR, filename); const dstPath = path.join(CLAIMS_ARCHIVE_DIR, filename); if (fs.existsSync(srcPath)) { fs.renameSync(srcPath, dstPath); } } ``` ### Technical Analysis Both `contentId` and `agent` can contain pathname separators or traversal components such as `..`. The code uses these values as part of a filename without validation. `path.join()` normalizes traversal components; it does not guarantee that the resulting path remains beneath `CLAIMS_DIR` or `CLAIMS_ARCHIVE_DIR`. During `claim`, `writeJSON()` may consequently write outside the intended claims directory when the normalized destination and required parent directories are accessible. Existing files can be overwritten because `fs.writeFileSync()` uses overwrite behavior by default. During `release` and `publish`, unsafe paths are passed to `fs.renameSync()`. This can move an unintended source file to an unintended destination, subject to filesystem layout, parent-directory existence, and process permissions. The environment-derived `agent` field is also unsafe, so protecting only the CLI content ID would not fully address the issue. ### Attack Pat ...[truncated 1343 chars]
Remediation
## Remediation Suggestions Apply strict validation to every value used in a claim filename: ```js function validateIdentifier(value, fieldName) { if ( typeof value !== 'string' || value.length < 1 || value.length > 100 || !/^[A-Za-z0-9_-]+$/.test(value) ) { throw new Error(`Invalid ${fieldName}`); } } ``` Validate both `contentId` and `agent` before constructing any path. In addition, enforce path containment defensively: ```js function containedPath(baseDir, filename) { const base = path.resolve(baseDir); const target = path.resolve(base, filename); if (!target.startsWith(base + path.sep)) { throw new Error('Path escapes the permitted directory'); } return target; } ``` Further hardening should include: 1. Generate storage filenames from encoded or hashed identifiers rather than raw user input. 2. Reject `/`, `\`, `..`, NUL characters, control characters, and absolute paths. 3. Verify source and destination containment before every write or rename. 4. Use restrictive filesystem permissions for editorial state. 5. Avoid following symlinks in writable claim directories; verify file types where appropriate. 6. Add tests for POSIX traversal, Windows separators, absolute paths, mixed separators, and symlink-based escape attempts.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/editorial.js:94
Finding
Race Condition in Claim Conflict Enforcement## Vulnerability Details **File Location**: `scripts/editorial.js`, lines 94-113 **Vulnerability Type**: Time-of-check to time-of-use race condition **Risk Level**: Medium ### Vulnerable Code ```js function cmdClaim(contentId, action, channel) { if (!contentId || !action || !channel) { console.error('Usage: editorial claim <content-id> <action> <channel>'); process.exit(1); } // Determine agent from environment or default to "unknown" const agent = process.env.OPENCLAW_AGENT || process.env.USER || 'unknown'; // Check for conflicts const conflict = findConflict(contentId, channel, agent); if (conflict) { console.error(`⚠️ CONFLICT: ${conflict.agent} already claimed ${contentId} on ${channel}`); process.exit(1); } const claim = { agent, content_id: contentId, action, channel, claimed_at: new Date().toISOString() }; const filename = `${contentId}-${agent}.claim`; const filepath = path.join(CLAIMS_DIR, filename); writeJSON(filepath, claim); } ``` ### Technical Analysis Conflict detection and claim creation are independent filesystem operations. No lock, atomic transaction, or exclusive-create primitive protects the interval between `findConflict()` and `writeJSON()`. Two agents can concurrently enumerate active claims and both observe that no conflicting claim exists. Each can then write its own claim file. Because filenames include the agent identity, different agents generally create separate files, leaving simultaneous claims for the same content and channel. For repeated claims by the same agent, the fixed filename also allows an existing claim to be overwritten rather than rejected. Git commits do not provide synchronization because they occur only after the filesystem write, and commit failures are ignored. ### Attack Path 1. Two agents start `claim` for the same `contentId` and `channel` at nearly the same time. 2. Agent A calls `findConflict()` before either claim file exi ...[truncated 827 chars]
Remediation
## Remediation Suggestions Make claim acquisition atomic rather than performing a separate check and write. Recommended options include: 1. Create one canonical lock file per normalized `contentId` and `channel`. 2. Open it with Node.js’s exclusive creation flag, such as `fs.openSync(path, 'wx')`; treat `EEXIST` as a conflict. 3. Store agent and expiry metadata only after exclusive creation succeeds. 4. For expired locks, use a carefully synchronized compare-and-replace procedure. 5. Alternatively, use a transactional data store or a lock library with well-defined stale-lock handling. 6. Write state to a temporary file and atomically rename it where multi-field updates are required. 7. Add parallel-process tests that launch many simultaneous claims and assert that exactly one succeeds. 8. Do not rely on Git commits as a locking mechanism unless explicit compare-and-swap and merge-conflict handling are implemented.

other

Note
Location
scripts/editorial.js:195
Finding
Published-Content Check Returns a Success Exit Status## Vulnerability Details **File Location**: `scripts/editorial.js`, lines 195-201 **Vulnerability Type**: Fail-open duplicate-publication control **Risk Level**: Low ### Vulnerable Code ```js const published = ledger.find(e => e.content_id === contentId && e.channel === channel); if (published) { console.log(`ℹ️ Already published: ${contentId} on ${channel}`); console.log(` Published at: ${published.published_at}`); console.log(` URL: ${published.url}`); process.exit(0); } ``` The documented contract in `SKILL.md`, lines 85-88, states: ```markdown **Exit codes:** - `0`: Safe to publish - `1`: Conflict or already published ``` ### Technical Analysis The `check` command identifies previously published content but exits with status `0`. This contradicts the documented interface, where status `0` means publication is safe and status `1` means a conflict or prior publication was found. Shell scripts and automated agents commonly use only an exit status to decide whether to continue. Such callers will interpret an already-published result as success and may proceed, despite the informational message printed to standard output. ### Attack Path 1. A content-and-channel pair already exists in `ledger.json`. 2. Automation runs `editorial check <content-id> <channel>`. 3. The command finds the existing ledger entry. 4. It prints “Already published” but exits with status `0`. 5. The caller interprets the successful status as authorization to continue. 6. The same content may be published again. ### Impact Assessment The flaw can cause duplicate publication and undermine workflow integrity. It does not grant filesystem or operating-system privileges and does not itself expose confidential data. The affected scope is automation that relies on the documented exit-code contract rather than parsing human-readable output.
Remediation
## Remediation Suggestions Return a nonzero exit status when prior publication is detected: ```js if (published) { console.error(`Already published: ${contentId} on ${channel}`); console.error(`Published at: ${published.published_at}`); console.error(`URL: ${published.url}`); process.exit(1); } ``` Also: 1. Add automated tests that assert exact exit codes for safe, conflicting, and already-published outcomes. 2. Keep documentation and implementation synchronized. 3. Consider distinct codes, such as `1` for a conflict and `2` for already published, if callers need to distinguish these states. 4. Provide a machine-readable output mode for agent automation rather than requiring parsing of decorated console messages.
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (28)

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/editorial.js status
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The report contains operational instructions telling users to run local shell and Node.js commands, including a boot hook and commands that modify on-disk state such as claim, release, and publish. Because the markdown does not clearly warn that these commands execute code and can change files, create git commits, and alter persistent coordination state, a user may run them blindly and trigger unintended side effects.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill instructs the agent to run commands that write claim and publication state to persistent files and automatically create git commits, but it does not clearly warn the user that these actions have lasting side effects. In an agentic environment, hidden persistence and automatic version-control commits can cause unintended data modification, audit noise, disclosure through commit history, and difficult-to-reverse state changes.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The script invokes shell commands via execSync using string interpolation and automatically performs `git add .` and `git commit` on every operation. This creates risk because untrusted values such as the commit message content can influence shell execution, and the implicit commit behavior may persist unintended files or sensitive data without the caller realizing it.

Intent-Code Divergence

Low
Confidence
84% confidence
Finding
The skill documentation describes `ledger.json` as an "Append-only publication log" where entries are permanent, establishing an immutability guarantee. Later, the troubleshooting section explicitly suggests manually editing `ledger.json` to republish content, which contradicts that stated intent and weakens the claimed audit semantics.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/editorial.js:27